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

YAGNI: как не писать код «на будущее», которое не наступит

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

7 апреля 20269 мин чтенияПрактикам
YAGNI: как не писать код «на будущее», которое не наступит
0 сценариевподтверждённых заранее
3 повтораразумный порог для обобщения

YAGNI, «you aren't gonna need it», появился в практике экстремального программирования как противовес привычке проектировать систему заранее под все мыслимые варианты развития. Принцип звучит почти провокационно просто: не добавляй возможность, пока в ней нет текущей, подтверждённой потребности. Проблема не в формулировке, а в том, что большинство разработчиков искренне уверены, что их конкретное предвидение будущего отличается от чужого неоправданного усложнения.

Чем гипотеза отличается от требования

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

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

// Написано 'на будущее': второго поставщика ещё нет
interface PaymentGateway
{
    public function charge(float $amount, string $currency): string;
    public function refund(string $transactionId): void;
    public function getSupportedCurrencies(): array;
    public function getMinimumAmount(string $currency): float;
}

final class StripeGateway implements PaymentGateway
{
    // единственная реализация интерфейса на весь проект
}

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

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

Правило трёх повторений

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

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

СитуацияYAGNI говоритПочему
Один способ оплаты в проектеНе вводить интерфейс заранееНет второй реализации, которую нужно отделить
Конфигурационный флаг под функцию, которую попросят через полгодаНе добавлять флаг сейчасТребование не подтверждено, форма флага может не совпасть с реальным запросом
Одна и та же проверка скопирована третий раз подрядВынести в общий методПовторение реальное, а не гипотетическое, и уже оплачено тремя копиями

Где YAGNI путают с отказом от проектирования

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

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

Хороший тест перед добавлением гибкости: если убрать этот параметр или интерфейс прямо сейчас, что-то реально сломается в требованиях сегодняшнего дня? Если ответ «нет, но может понадобиться позже», это сигнал YAGNI, а не повод оставить код как есть.
Вы в начале разбораДальше: разбор компромиссов, чек-лист и ответы на частые вопросы.

Цена кода, написанного на будущее

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

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

Когда предусмотреть что-то заранее оправдано

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

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

  1. Сформулировать явно, какое будущее требование оправдывает текущую гибкость
  2. Проверить, подтверждено ли это требование кем-то с полномочиями, или это только предположение разработчика
  3. Если требование гипотетическое, написать максимально простой код под сегодняшнюю задачу
  4. Вернуться к обобщению только тогда, когда появится второй реальный случай использования

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

Как разговаривать с коллегами о YAGNI на код-ревью

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

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

YAGNI и рефакторинг под изменившееся требование

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

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

YAGNI не является утверждением, что будущее не важно. Это утверждение о том, что оплачивать неопределённое будущее сегодня почти всегда дороже, чем оплатить его позже, когда оно станет определённым требованием с конкретной формой и сроком.

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

YAGNI означает, что не нужно проектировать архитектуру заранее?

Нет. Принцип не против хорошей структуры кода, он против функциональности, написанной под сценарий, который ещё не подтверждён. Явные границы между слоями и понятные имена классов не нарушают YAGNI.

Как понять, что гибкость в коде оправдана, а не написана «на будущее»?

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

Когда стоит выносить повторяющийся код в общую абстракцию?

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

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

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

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