Skip to content

Latest commit

 

History

History
238 lines (184 loc) · 11.1 KB

File metadata and controls

238 lines (184 loc) · 11.1 KB

Аргументы

Чем меньше, тем лучше

Методы с большим числом аргументов сложнее читать, тестировать и использовать. Правило трёх: метод не должен принимать больше трёх параметров. Если больше — разделите.

// Плохо [✗]
$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 работают с объектами, а не с простыми значениями. Это упрощает добавление новых параметров и изменений в модель, не затрагивая логику работы методов, а также облегчает тестирование.