Новое в журнале. Маленькие pull request: почему их быстрее ревьюят и как резать задачу
Принципы и архитектура

KISS на практике: как простота становится инженерным решением

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

18 ноября 20258 мин чтенияПрактикам
KISS на практике: как простота становится инженерным решением
5 источниковлишней сложности
1 вопросперед абстракцией

KISS, «keep it simple, stupid», звучит как лозунг для плакатов, а не как инженерный принцип. Проблема не в формулировке, а в том, что простоту обычно путают с количеством строк. Короткий код, напичканный тернарными операторами и неявными побочными эффектами, может быть куда сложнее для понимания, чем десять скучных, но прямых строк.

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

Простота это предсказуемость, а не краткость

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

Код можно назвать простым, если человек, впервые открывший файл, восстанавливает его логику без прыжков по пяти слоям абстракции и без вопроса «а что здесь вообще происходит». Простота измеряется не числом символов, а количеством контекста, который нужно удержать в голове одновременно.

Полезный тест на простоту: попросить коллегу, не знакомого с участком кода, объяснить вслух, что делает функция, глядя только на её сигнатуру и первые несколько строк. Если объяснение требует постоянных «а, подождите, тут ещё нужно учесть», это сигнал, что реальная сложность задачи превышает то, что видно в коде, и часть логики спрятана в неочевидных местах.

// 'Компактно', но требует распаковки в голове
$status = $user->isActive() ? ($user->hasSubscription() ? 'premium' : 'free') : 'blocked';

// Прямее: читается сверху вниз без вложенности
function resolveUserStatus(User $user): string
{
    if (!$user->isActive()) {
        return 'blocked';
    }

    return $user->hasSubscription() ? 'premium' : 'free';
}
Главная мысльПростой код не значит короткий. Простой код значит, что следующий разработчик поймёт его намерение за один проход, а не за час с дебаггером.

Источник первый: гибкость под гипотетическое будущее

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

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

Источник второй: собственные мини-фреймворки

Желание решить задачу «правильно и на вырост» часто выливается в написание собственного роутера, ORM-обёртки или системы событий внутри обычного проекта. Пока автор рядом, это работает. Как только он уходит в отпуск или меняет команду, поддерживать самодельный слой приходится людям, которые не участвовали в его создании и не понимают исходных решений.

Отличить оправданную инфраструктуру от самодельного фреймворка помогает простой вопрос: решает ли этот слой задачу, которой нет готового зрелого решения, или просто переизобретает то, что уже есть в экосистеме языка под другим именем. Если ответ «переизобретает», это почти всегда дороже в долгосрочной поддержке, чем кажется в момент написания.

Сигнал сложностиЧто обычно скрываетсяКак проверить
Метод принимает флаг bool $modeНа самом деле два разных поведения в одном местеРазделить на два метода с понятными именами
Пять уровней вложенных интерфейсовАбстракция под единственную реализациюПосчитать реальное число реализаций интерфейса
Конфиг с десятками переключателейПопытка закрыть сразу все будущие случаиОставить только то, что реально используется сегодня
Вы в начале разбораДальше: разбор компромиссов, чек-лист и ответы на частые вопросы.

Источник третий: неявная магия

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

// Магия: поведение зависит от скрытого состояния контейнера
class Report
{
    public function __get(string $name)
    {
        return $this->container->resolve($name);
    }
}

// Явно: видно, что реально используется
class Report
{
    public function __construct(
        private readonly Formatter $formatter,
        private readonly Logger $logger,
    ) {
    }
}

Явное внедрение зависимостей в конструктор длиннее на две строки, но полностью снимает вопрос «а что ещё доступно внутри этого объекта». Магический __get экономит код в моменте написания и отбирает время у каждого, кто будет читать класс позже, включая самого автора через полгода.

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

Источник четвёртый: преждевременная оптимизация

Кеширование, которое ничего не кеширует на реальных объёмах данных, или ручная оптимизация запроса, который выполняется раз в сутки, добавляют код без пользы. Простое решение «сначала запрос как есть, потом профилирование, затем точечная оптимизация» почти всегда выигрывает у заранее усложнённой схемы.

Цена такой сложности не только в лишних строках. Каждый слой кеша требует отдельной логики инвалидации, которую тоже нужно поддерживать и тестировать. Если данные под кешем на практике меняются чаще, чем ожидалось при проектировании, устаревший кеш начинает отдавать неверные значения, и диагностировать такую ошибку куда труднее, чем медленный, но предсказуемый прямой запрос.

  • Профилировать перед оптимизацией, а не после интуиции
  • Кешировать только то, что реально вызывается часто и стоит дорого
  • Удалять оптимизацию, если нагрузка не подтвердилась на практике

Источник пятый: избыточная обработка ошибок

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

Частный случай этой проблемы: обработка ошибки на том уровне, где у кода ещё нет достаточного контекста, чтобы принять разумное решение. Низкоуровневая функция, которая перехватывает исключение, логирует его и возвращает null, прячет проблему от верхних слоёв, которые могли бы обработать её осмысленно, например показать пользователю понятное сообщение вместо тихого исчезновения результата.

Как резать сложность на практике

  1. Задать вопрос: решает ли эта абстракция задачу, которая существует сегодня, или задачу, которая может появиться
  2. Посчитать реальное число мест использования перед тем, как обобщать код
  3. Проверить, можно ли объяснить функцию одним предложением без «а ещё, если»
  4. Удалить код, который не используется, вместо того чтобы оставлять его «на всякий случай»

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

Полезная привычка, которая закрепляет KISS в команде: требовать на код-ревью короткое объяснение, зачем добавлена абстракция, кеш или конфигурационный флаг, прямо в описании изменений. Если автору сложно сформулировать причину в одном предложении, скорее всего, причины для усложнения ещё не появилось, и решение можно оставить максимально прямым до того момента, когда реальная потребность станет очевидной. Такая привычка стоит почти ничего, но за несколько месяцев заметно снижает число абстракций, оставшихся в проекте без единого реального потребителя.

Частые вопросы

KISS значит писать меньше кода?

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

Как отличить нужную гибкость от лишней сложности?

Проверить, существует ли сценарий, под который добавлена гибкость, прямо сейчас. Если он гипотетический, абстракцию лучше отложить до момента, когда сценарий реально появится.

Противоречит ли KISS использованию паттернов проектирования?

Нет, если паттерн решает реальную проблему данного кода. Противоречие возникает, когда паттерн добавлен ради вида, а не ради задачи.

Код, который не стыдно передать

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

Случайный разбор