Совершенный код: явные и неявные параметры функций

Совершенный код: явные и неявные параметры функций главное изображение

В динамических языках есть два основных подхода при проектировании входных параметров функций: первый – использовать явные, позиционные аргументы, и второй – передавать структуру, внутри которой должно находиться все то, что ожидает функция. Явный и неявный способы передачи одинаково часто встречаются в реальном коде и, при этом, не всегда понятно, какой способ стоит предпочесть для конкретной функции. Именно об этом мы и поговорим.

Для начала рассмотрим один простой пример – функцию, которая находит максимальное число среди переданных:

// Явные параметры
const max1 = (n1, n2) => (n1 > n2 ? n1 : n2);
max1(1, 2); // 2

// Неявные параметры
const max2 = ({ n1, n2 }) => (n1 > n2 ? n1 : n2);
max2({ n1: 1, n2: 2 }); // 2

Явные параметры делают код самодокументируемым: по сигнатуре функции сразу понятно, какие аргументы ожидаются. А хорошие названия подсказывают их типы. Небольшое удобство добавляет метод apply, с помощью которого удобно вызывать функцию, если сами параметры хранятся в массиве:

// Встроенная в js функция
Math.max(3, 2); // 3

const numbers = [3, 2];
Math.max.apply(null, numbers);
// Вместо Math.max(numbers[0], numbers[1]);

Теперь про недостатки. Главный недостаток — в жесткой завязке на аргументы. При изменении количества аргументов, включая добавление и удаление параметров по умолчанию, придется переписывать все места, где функция используется. Особенно это актуально в случае высокоуровневых функций, сквозь которые проходит множество данных:

// У пользователя может быть десяток-другой полей
// Часть из них необязательные
// Все это может меняться в процессе жизни программы
// Жесткая завязка на порядок, хотя по смыслу порядка тут нет
const user = new User(firstName, lastName, email);
user.save();

При использовании неявных аргументов ситуация меняется на противоположную. Резко падает уровень самодокументируемости. Без описания или анализа содержимого функции сложно понять, что ей можно передавать. С другой стороны, у таких функций редко меняется определение, так как количество передаваемых данных расширяется автоматически:

const data = { firstName, lastName, email };
// Порядок теперь не важен
// Можно передавать любые данные, главное чтобы функция их обработала
const user = new User(data);
user.save();

Какой способ лучше в той или иной ситуации? Исходя из вышесказанного, можно заметить, что явные аргументы лучше всего работают с простыми и низкоуровневыми функциями, например математическими. Именно поэтому все функции для работы со строками или числами во всех языках имеют явные аргументы. То же самое касается практически всех остальных встроенных функций и методов.

Это относится и к пользовательским функциям, которые имеют четкую, редко меняющуюся структуру. Обычно с небольшим числом аргументов.

// Функция записывающая данные в файл
writeFile('path/to/file', 'data');

С неявными чуть сложнее. Вот список ситуаций, в которых их использовать лучше:

  • Когда функция не использует передаваемые данные напрямую, а транслирует их дальше по цепочке вызовов. В такой ситуации явные аргументы будут только мешать из-за необходимости их постоянно править, при том, что функция ими не пользуется.
  • Когда данных много и, особенно, если они могут часто меняться в процессе. Или среди этих параметров много необязательных. Пример с пользователем как раз об этом. Нередко данные в таких ситуациях приходят в виде объекта (ассоциативного массива) и их, в принципе, удобнее передавать всем скопом сразу. Кроме того, такой подход часто используют для конфигурирования:
    // http-клиент
    import axios from 'axios'
    
    const url = 'https//ru.hexlet.io/users';
    const data =  {
      firstName: 'Fred',
      lastName: 'Flintstone',
    };
    
    // Создание пользователя
    axios({ method: 'post', url, data, });
    
    // У axios кстати есть и явный интерфейс
    // Он используется в простых случаях
    axios.post(url, data);
    
  • Библиотечные функции, которые могут работать с произвольными параметрами и структурами. Частая ситуация — это ORM или библиотека для запросов к какой-нибудь системе, например, GitHub. В этой ситуации разработчик библиотеки просто не знает и часто не может знать, что и как собирается делать пользователь, поэтому оставляет конкретное наполнение на усмотрение тех, кто использует его код:
    // Клиент для запросов к Github API
    import Octokit from '@octokit/rest';
    
    const octokit = new Octokit();
    
    octokit.repos
      .listForOrg({ // Метод принимает на вход объект с параметрами
        org: 'octokit',
        type: 'public',
      });
    

В некоторых языках, таких как python или ruby, существуют специальные именованные параметры. Они явно прописываются в определении как позиционные параметры, что делает их самодокументируемыми, с другой стороны, их можно передавать в любом порядке указывая имя параметра при вызове. Такой способ не заменяет остальные, но делает код проще в некоторых случаях.

Но не все ситуации можно так легко разложить. Там, где непонятно, что делать, рекомендуется работать с явными аргументами, и только потом по необходимости переходить к неявным. Главный ориентир – здравый смысл.

Специально для сайта ITWORLD.UZ. Новость взята с сайта Блог Хекслета