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 — два способа её нарушить, а высокая связность и низкая связанность — то, к чему это всё сводится. Такая карта показывает, что принципы у вас в системе, а не списком.
Если спросят «как вы решаете, что пора рефакторить» — отвечайте через частоту изменений и стоимость правки, а не через эстетику. И упомяните обязательное условие: тесты до, поведение неизменно, мелкими шагами внутри задач.