Получить доступ

DRY, KISS, YAGNI и качество связей

Дублирование знания против дублирования текста, цена преждевременной абстракции, связность и связанность, закон Деметры, каталог code smells и когда рефакторить.

DRY: дублируется знание, а не текст

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

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

Это называют случайным (совпадающим) дублированием. Критерий простой: спросите не «одинаково ли выглядит», а «изменятся ли эти места вместе и по одной причине». Если по разным — дублирование оставляют.

Полезные противовесы, которые стоит уметь назвать. Правило трёх (Фаулер): дублируйте спокойно, выделяйте абстракцию на третьем повторении, когда ось изменений уже видна. И формула Сэнди Метц: «дублирование дешевле неправильной абстракции» — из дублирования выход стоит одну правку, из неверно выбранной абстракции придётся выпутывать весь код, который на неё оперся.

У DRY есть измерение шире кода: одно и то же знание не должно жить одновременно в схеме БД, в DTO, в валидации на фронте и в документации, расходясь между ними. Часть таких дублей убирают генерацией (схема → код, OpenAPI → клиент), а не копированием.

// Случайное дублирование: код совпал, причины изменения разные.
BigDecimal retailDiscount(Order o) { return o.total().multiply(new BigDecimal("0.10")); }
BigDecimal staffDiscount(Order o)  { return o.total().multiply(new BigDecimal("0.10")); }

// Преждевременное «устранение дубля» связывает два правила и порождает флаг:
BigDecimal discount(Order o, boolean staff) { ... }   // дальше будет второй флаг
// и ещё третий — это уже не абстракция, а развилка.

KISS и YAGNI: цена того, чего ещё нет

KISS (keep it simple) — из двух работающих решений выбирают то, которое проще понять при чтении, а не то, которое короче или изобретательнее. Мерило простоты — усилие следующего читателя, включая вас через полгода.

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

Типичные жертвы YAGNI в Java-проектах: интерфейс с единственной реализацией «на случай второй БД», собственный слой абстракции над ORM, конфигурируемость через пропертис у того, что никто ни разу не переключал, и обобщённый «фреймворк» внутри продукта.

Важный нюанс для senior-уровня: YAGNI — про функциональность и спекулятивные точки расширения, но не про качество. Тесты, обработку ошибок, идемпотентность и понятные границы модулей нельзя откладывать под флагом YAGNI: это не будущая фича, а то, что делает изменение дешёвым в принципе. Отличать одно от другого и есть содержательная часть ответа.

Связность и связанность

Два взаимодополняющих понятия, к которым сводится большинство разговоров о структуре. Связность (cohesion) — насколько элементы внутри модуля относятся к одной задаче; её хотят высокой. Связанность (coupling) — насколько сильно модуль завязан на устройство других; её хотят низкой.

Высокая связность и низкая связанность — это, по сути, операционное определение хорошего разбиения: изменение одной бизнес-причины затрагивает один модуль (высокая связность) и не расходится волной по остальным (низкая связанность).

Связанность бывает разной силы, и полезно называть градации. Самая безобидная — по данным: модули обмениваются простыми параметрами. Хуже — управляющая связанность, когда один модуль флагом диктует другому, какую ветку выполнять (тот самый boolean-параметр из примера с DRY). Совсем плохо — общая связанность через разделяемое изменяемое состояние и содержательная, когда модуль лезет во внутренности другого.

Для архитектурного уровня стоит знать метрики Мартина: afferent coupling (сколько модулей зависят от вас) и efferent coupling (от скольких зависите вы). Модуль, от которого зависят многие, обязан быть стабильным — то есть меняться редко и состоять преимущественно из абстракций. Отсюда и требование DIP держать в межслойных интерфейсах абстракции, а не детали.

Закон Деметры и «Tell, don't ask»

Закон Деметры (принцип наименьшего знания): метод должен обращаться только к «непосредственным друзьям» — полям своего объекта, своим параметрам, созданным им объектам, — но не путешествовать по чужим графам.

Нарушение легко узнать по цепочке вида order.getCustomer().getAddress().getCity().getName(). Такая цепочка фиксирует в вызывающем коде всю структуру чужих объектов: любое изменение по пути ломает вызов, а замокать это в тесте — отдельное упражнение.

Лечение — «Tell, don't ask»: не вытаскивайте данные, чтобы принять решение снаружи, а попросите объект сделать работу. Вместо вытягивания города через четыре геттера — order.deliveryCity(). Вместо if (account.getBalance() >= amount) { account.setBalance(...) } — account.withdraw(amount), где инвариант проверяется внутри.

Частая ошибка на собеседовании — объявить нарушением любую цепочку вызовов. Fluent-интерфейсы нарушением не являются: у StringBuilder.append(...).append(...) и у стримов каждый вызов возвращает тот же самый объект или ту же абстракцию, а не раскрывает чужую структуру. Закон Деметры запрещает знание о графе чужих объектов, а не точки в строке.

// Нарушение: вызывающий знает всю структуру графа.
String city = order.getCustomer().getAddress().getCity().getName();

// Tell, don't ask: спрашиваем объект, а не разбираем его на части.
String city = order.deliveryCity();

// Не нарушение: fluent-цепочка возвращает ту же абстракцию.
String s = new StringBuilder().append("a").append("b").toString();

Каталог code smells

Запах кода — не баг, а признак, что структура сопротивляется изменениям. Уметь называть их своими именами полезно: это язык, на котором обсуждают рефакторинг.

Пара запахов прямо продолжает SRP. Divergent change — один класс приходится править по разным причинам (нарушение SRP внутри модуля). Shotgun surgery — зеркальный случай: одна бизнес-причина заставляет править десяток классов, то есть ответственность размазана. Первое лечат разделением, второе — сбором логики в одно место.

Feature envy — метод больше интересуется данными чужого класса, чем своими: перебирает его геттеры и что-то считает. Обычно метод надо перенести туда, где живут данные.

Primitive obsession — важные понятия домена живут как String и long: идентификатор заказа, деньги, email. Отсюда путаница аргументов одинакового типа и разъезжающаяся валидация. Лечится типами домена (OrderId, Money, Email), а в современной Java для этого удобны record.

Anemic domain model (Фаулер) — сущности состоят из полей и геттеров/сеттеров, а вся логика вынесена в *Service. Формально ООП, фактически процедурный стиль: инварианты никто не охраняет, и одно и то же правило дублируется по сервисам. Это не всегда ошибка — в CRUD-прослойке анемичная модель уместна, — но на собеседовании ценят умение объяснить, где проходит граница.

Остальной ходовой набор: длинный метод и длинный список параметров, большой класс, data clumps (одни и те же три поля всегда ходят вместе — значит, это объект), speculative generality (абстракция, введённая под несуществующее требование).

Когда рефакторить

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

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

Полезный критерий приоритета: рефакторить в первую очередь то, что часто меняется. Уродливый, но стабильный модуль, который никто не трогал два года, ничего не стоит команде; а вот запутанный участок, где каждую неделю правки, съедает время постоянно. Совмещение частоты изменений (по истории git) со сложностью даёт неплохую карту, куда вкладываться.

И обратная сторона: рефакторинг конкурирует за время с фичами, поэтому его тоже нужно уметь обосновать — через частоту изменений, время на внесение правки, количество инцидентов в этом модуле.

Частые заблуждения

Понимать DRY как «нигде не должно быть двух похожих строк» и склеивать код, который совпал случайно.

Прикрывать словом YAGNI отказ от тестов, обработки ошибок и внятных границ. YAGNI — про спекулятивные фичи и точки расширения, а не про качество.

Считать любую цепочку вызовов нарушением закона Деметры, включая fluent-билдеры и стримы.

Считать анемичную модель безусловной ошибкой: в тонком CRUD-слое она нормальна, вопрос в том, есть ли в домене инварианты, которые некому охранять.

Называть рефакторингом изменение, которое меняет поведение, и делать его без тестов.

Как отвечать на собеседовании

По DRY не ограничивайтесь расшифровкой аббревиатуры. Сильный ответ звучит так: DRY — про единственный источник знания, а не про совпадение текста; случайное дублирование объединять нельзя; правило трёх и «дублирование дешевле неправильной абстракции» — про то, когда абстракцию всё-таки вводят.

По YAGNI приводите разложение цены (постройка, задержка, сопровождение, исправление) и обязательно оговаривайте границу с качеством — это отличает ответ senior от заученного лозунга.

Держите наготове связку: SRP — про одну причину изменения, divergent change и shotgun surgery — два способа её нарушить, а высокая связность и низкая связанность — то, к чему это всё сводится. Такая карта показывает, что принципы у вас в системе, а не списком.

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

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем