Методы с большим числом аргументов сложнее читать, тестировать и использовать. Правило трёх: метод не должен принимать больше трёх параметров. Если больше — разделите.
// Плохо [✗]
$fileSystem->write(
'/path/to/file.txt', // Путь до файла
true, // Перезапись файла
'Пример данных', // Содержимое
'UTF-8', // Кодировка
true // Включаем логирование
);Если у метода четыре, пять, а то и шесть параметров — становится сложно понять, что есть что, в каком порядке это передавать и как вообще это использовать. Особенно это усугубляется, когда имена аргументов очень похожи.
Даже если вы напишете великолепный комментарий перед методом, человек читающий код будет вынужден каждый раз к нему возвращаться.
При проектировании методов порядок аргументов имеет значение. Один из самых простых и эффективных способов сделать его чище — располагать необязательные параметры в конце.
Рассмотрим пример:
// Плохо [✗]
$fileSystem->write(
'/path/to/file.txt', // Путь до файла
null, // Перезапись файла
'Пример данных', // Содержимое
);Чтобы просто записать файл, нам приходится явно указывать null — значение, которое на самом деле нам не нужно.
Куда лучше такой вариант:
// Хорошо [✓]
$fileSystem->write(
'/path/to/file.txt', // Путь до файла
'Пример данных', // Содержимое
);А если нужно изменить поведение по умолчанию, мы просто добавим третий параметр:
// Плохо [✗]
$fileSystem->write(
'/path/to/file.txt', // Путь до файла
'Пример данных', // Содержимое
true, // Перезапись файла
);В этом случае метод будет принимать только обязательные параметры, а необязательные будут в конце. Это делает код чище и понятнее. Так как их можно не указывать, если они не нужны.
Иногда метод требует не один-два, а сразу пять или больше параметров. Передавать всё списком в строго заданном порядке — не лучшая идея. Легко перепутать аргументы, особенно если они одного типа. К тому же вызов такого метода выглядит пугающе и плохо читается.
Первое, что приходит на ум это использование ассоциативного массива:
// Плохо [✗]
$fileSystem->write(
'/path/to/file.txt',
'Пример данных',
[
'encoding' => 'UTF-8',
'overwrite' => true,
'debug' => true,
]
);Это отвратительный способ. Такой подход не даёт информации о том, какие параметры действительно ожидаются, и не позволяет IDE подсказывать возможные опции. Более того, здесь нет проверки типов — любые ошибки проявятся только во время выполнения. Это усложняет отладку и увеличивает вероятность багов.
Другая популярная попытка — создать объект, инкапсулирующий значения. Например:
$config = new Config($encoding, $overwrite, $debug);
// Пример использования
$fileSystem->write(
'/path/to/file.txt', // Путь до файла
null, // Перезапись файла
$config, // Объект с метаданными
);Это лишь видимость решения. Мы создали объект, который сам по себе бессмысленен: он не содержит поведения и не добавляет
никакой бизнес-логики. Фактически, это тот же массив, только завернутый в класс. Польза от него — разве что
автодополнение в IDE. Но теперь мы должны создавать или таскать этот объект везде, где вызываем метод write, что
только усложняет код.
Если язык поддерживает именованные аргументы и их количество очень-очень ограничено, стоит использовать их:
$fileSystem->write(
'/path/to/file.txt',
'Пример данных',
debug: true, // Именованный параметр
);Это уже лучше: вызов становится самодокументируемым, и порядок аргументов не имеет значения.
Но есть гораздо более выразительный и управляемый способ — fluent-интерфейс. Это объект, методы которого возвращают самого себя, позволяя вызывать их цепочкой:
// Хорошо [✓]
$fileSystem
->path('/path/to/file.txt')
->encoding('UTF-8')
->overwrite(true)
->debug(true)
->write('Пример данных');В этом подходе сразу видно, что происходит. Каждый шаг отделён, названия методов описывают действия, и вся цепочка читается как связный набор настроек. Такой стиль легко расширяется, хорошо покрывается тестами и открывает дорогу к более гибкой архитектуре.
Стоит отдельно упомянуть, что многие разработчики и известные авторы, например, Роберт Мартин — автор "Чистого кода", считают использование булевых аргументов признаком плохого тона. И предлагают создавать отдельный метод вместо передачи булева значения.
Например, вместо:
$fileSystem->write(
'/path/to/file.txt', // Путь до файла
'Пример данных', // Содержимое
true // перезаписать файл
);Предпочтительнее сделать:
$fileSystem->reWrite(
'/path/to/file.txt', // Путь до файла
'Пример данных', // Содержимое
);Однако я бы поспорил с этой категоричной рекомендацией. В ряде случаев булевый параметр — вполне удобный и компактный способ управления поведением метода, особенно если код остаётся понятным.
Но если булевый аргумент существенно меняет поведение метода, превращая его фактически в две разные функции — тогда действительно стоит рассмотреть разделение.
Строки, булевы, числа очень удобны в начале разработки, но с течением времени логика усложняется, и эти простые значения не справляются.
Что раньше было флагом true, теперь требует дополнительных условий:
если админ, если включён режим отладки, если пользователь подтвердил e-mail.
Скалярные значения не умеют расти. Они не подстраиваются под новые требования. А объект — может. Он расширяется методами, валидирует себя, хранит контекст и смысл.
Рассмотрим пример списка исключений. Вместо того чтобы передавать набор строк, лучше использовать объект, который сам знает, как представлять себя:
// Плохо [✗]
class ExcludeList
{
public function add(
string $itemName,
string $itemIdentityName,
string $itemIdentityValue
): void
{
// ...
}
public function has(
string $itemName,
string $itemIdentityName,
string $itemIdentityValue
): bool
{
// ...
}
}Вместо того чтобы передавать несколько строковых значений, можно использовать уже существующий объект или создать новый, который сам решит, как обработать добавление и поиск элемента:
// Хорошо [✓]
class ExcludeList
{
public function add(Model $model): static
{
$key = $model->getKey();
// ...
}
public function has(Model $model): bool
{
$key = $model->getKey();
// ...
}
}Теперь метод add и метод has работают с объектами, а не с простыми значениями.
Это упрощает добавление новых параметров и изменений в модель, не затрагивая логику работы методов, а также облегчает
тестирование.