Programming Languages
February 27, 2021

Сколько надо параметров?

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

Для ответа на этот вопрос попробуем посмотреть на явление с разных сторон.

Когнитивные трудности

Начнем с понятия «Кошелек Миллера» (Закон Миллера), которое гласит, что в кратковременной памяти может в среднем держаться 5±2 предмета. Я, конечно, упрощаю, но смысл такой.

То есть, с точки зрения восприятия 3-5 параметров — достаточно. Читатель, глядя как на сигнатуру функции, так и в место вызова не испытывает особой нагрузки при анализе. Но все не так просто.

Исчезающие параметры

В некоторых случаях первые один-два, а то и три параметра функции отдаются под нужды библиотеки, в которой функция находится, и тогда они как бы «растворяются» в самой функции:

cairo_save (cr);
cairo_translate (cr, x + width / 2., y + height / 2.);
cairo_scale (cr, width / 2., height / 2.);
cairo_arc (cr, 0., 0., 1., 0., 2 * M_PI);
cairo_restore (cr);

Пример взят из библиотеки Cairo. Первый параметр, как видно, всегда указатель на текущий графический контекст. Он передается в функции библиотеки почти в 100% случаях, и, как следствие, на количество параметров функции фактически не влияет.

Кстати говоря, именно так компилятор поступает, генерируя код для нестатических методов классов. Первым (или последним, зависит от соглашениях о вызовах) параметром функции передается указатель на экземпляр класса, т.е. this. В зависимости от языка этот параметр может называться self, object или еще как-то по-другому, сути это не меняет. Такая хитрость применяется в генерации, синтаксически этот параметр не используется при описании и вызове метода.

А если надо больше?

Идем дальше. В некоторых случаях бoльшее количество параметров оправдано логикой кода. Например, функции-агрегаторы, собирающие какие-то данные в структуру. Хороший пример — функция CreateUser(), собирающая из входящих параметров информацию о пользователе и возвращающая структуру.

То есть, функция вроде

function CreateUser(
  string name, 
  string surname,
  string login, 
  string email, 
  string phone) -> User {

  ...тело функции...
  
}

вполне может иметь количество параметров, существенно отличающееся от значений 3-5.

Но здесь важно понимать, что для сохранения работоспособности кода и его читаемости, а также для безболезненной или малозатратной модификации кода, необходимо помнить о проблемах такого подхода:

  1. Необходимо понимать, что список параметров функции-агрегатора может начать меняться (расти или уменьшаться) со временем и этот список придется поддерживать не только в описании, но и разумеется, в точках вызова, что может быть очень нетривиальной задачей. Кроме того, если код этой функции покрыт тестами (вы же пользуетесь тестированием, не так ли?), то их тоже придется править, причем этот процесс обычно разнесен по времени с модификацией основного кода и неприятно получить упавшие тесты после успешного локального билда.
  2. При увеличивающемся количестве параметров функции в них самих появляется внутренняя логика, невидимая в точке вызова. Приведу пример. Допустим, функция CreateUser() создает разных пользователей в зависимости от наличия или отсутствия записи о телефоне (т.е. phone == "" при вызове функции). В точке вызова функции это не проявляется никак. То есть читатель для понимания факта работы с телефоном должен предварительно погрузиться во внутренности функции CreateUser().

Решений может быть несколько, рассмотрим два хорошо себя зарекомендовавших.

Модифицирующие функции

Первый способ — отказ от параметров в пользу модифицирующих функций:

function CreateUser(
  string name, 
  string surname,
  string login) -> User;

function setEmail(User u, string email) -> boolean;
function setPhone(User u, string phone) -> boolean;

В функции-агрегаторе мы оставляем параметры, которые мы вряд-ли когда-то изменим, а вот остальную логику выносим в отдельные функции или вспомогательные классы. Разумеется, некоторые вещи можно логически группировать, но в примере с номером телефона где-то в коде мы увидим такой конструкт:

var user = CreateUser("Александр", "Петров", "T34");
...
if (phone != "") {
  setPhone(user, phone);
} else {
  ...
}

И читатель, глядя на этот код, будет понимать происходящее гораздо быстрее, потому что логика работы здесь прописана явно.

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

Структура-параметр

Второй способ передачи большого количества параметров в функцию — использование агрегирующий структуры.

В программе создается структура данных, содержащая все параметры функции, и в функцию уже передается структура. Что-то вроде

type UserParams = struct {
  string name,
  string surname,
  string login,
  string email,
  string phone
}

function CreateUser(UserParams params) -> User {
  ...
}

Теперь мы можем безболезненно модифицировать структуру, оставляя неизменными как определение функции, так и точки вызова.

Введение в код дополнительной структуры имеет свою стоимость, расходуется память и объем кода. Хорошо это или плохо можно говорить после измерений программы в конкретном рабочем окружении, однако стоимость решения с лихвой окупается удобством сопровождения кода.

Переменное количество параметров

Отдельно стоит упомянуть variadic-параметры, т.е. параметр функции, позволяющий передать переменное количество параметров, включая нулевое.

Очевидный пример — функция printf() из C.

printf("Result=%d (value=%d)\n", result, value);

Здесь значения переменных result и value фактически представляют собой один параметр функции и передаются в функцию в виде массива параметров. Также можно вызвать printf() и с нулевым количеством параметров.

Для компилятора реализация variadic-параметров - одно из самых сложным мест. Здесь необходимо решить массу сложных моментов, как синтаксического, так и семантического свойства. Эта тема достойна отдельной статьи. Для текущего обсуждения достаточно понимать, что такие параметры можно представить в виде массива значений.

Хотя есть большой соблазн использовать variadic-параметры, лучше стараться этого не делать. Применение такой техники ограничено несколькими узкими местами вроде форматного вывода. В пользовательском же коде это может представлять собой попытку обойти или ослабить проверки системы типов компилятора, что редко идет на пользу читаемости программы.

Резюме

Давайте подведем итоги.

  1. Для функции оптимально иметь меньше 5 параметров.
  2. В некоторых случаях эта оценка может быть как уменьшена, так и увеличена, но делать это нужно осмысленно.
  3. К функциям-агрегатам предъявляются повышенные требования в связи с развитием кода программы.
  4. Переменное количество параметров может быть удобно, но в целом имеет ограниченные области применения.