DRY: Don’t repeat yourself
Введение
Большинство кодовых баз деградирует не из-за «плохих программистов», а из-за размножения знаний. Один и тот же бизнес-факт начинает жить сразу в нескольких местах: в сервисе, в контроллере, в SQL, в конфиге, в документации. Пока изменения редки — это почти незаметно. Но как только система начинает эволюционировать, дубли превращаются в источник расхождений: одно место обновили, другое забыли, третье пофиксили костылём.
DRY (Don’t Repeat Yourself) — принцип разработки, цель которого уменьшить повторение изменяемой информации, заменяя его более устойчивыми абстракциями или нормализацией данных.
Противоположный лагерю DRY часто в шутку приписывают WET — «Write Everything Twice / We Enjoy Typing / Waste Everyone’s Time».
WET — это не стиль, а симптом: знание размазано по проекту, поэтому любой чих требует править десять файлов.
Как DRY пришёл в программирование
Хотя аббревиатура DRY стала популярной в конце 1990-х, сама идея «одно знание — одно место» значительно старше.
1) Данные и нормализация (1960–1970-е).
Когда индустрия столкнулась с большими общими базами данных, быстро выяснилось: избыточность порождает несогласованность. Реляционная модель и нормализация (в ранних работах Эдгара Кодда) формализовали стремление устранять дубли и приводить данные к канонической форме.
Это тот же DRY, только на уровне данных: если адрес клиента хранится в пяти таблицах, рано или поздно он станет пятью разными адресами.
2) Декомпозиция и «одна ответственность знания» (1970-е и далее).
Эволюция модульности, сокрытие решений, идея разделения на компоненты — всё это двигалось в сторону того, чтобы изменяемые решения имели ясный «центр тяжести», а не были размазаны по коду. В терминах DRY: знание должно иметь авторитетный источник, а остальные места — быть лишь «представлением» этого источника.
3) Формулировка DRY (конец 1990-х).
Современное, ставшее классическим определение DRY закрепили Энди Хант и Дэйв Томас в The Pragmatic Programmer (1999):
«каждый кусочек знания должен иметь единственное, недвусмысленное, авторитетное представление в системе»
Параллельно в инженерной культуре закреплялась родственная формула
«Once and Only Once» — «однажды и только однажды».
Именно в этом месте важно не перепутать DRY с популярным упрощением.
Суть принципа
DRY — не про «не повторяй код». DRY — про не повторяй знание.
Код может повторяться строка в строку и не нарушать DRY, если в нём нет повторения изменяемого знания (например, два тривиальных геттера). И наоборот: код может быть разный, но нарушать DRY, если он по-разному выражает один и тот же бизнес-факт.
Ключевые термины DRY:
- single source of truth — один источник истины (SSOT)
- «изменяемая информация» (то, что меняется требованиями, правилами бизнеса, интеграциями)
- «авторитетное представление» — место, которое отвечает за корректность знания
Где чаще всего нарушают DRY
- Валидации и правила домена, размноженные по контроллерам/сервисам/DTO.
- Маппинг и преобразования, скопированные между слоями.
- Константы/магические значения, которые «на самом деле» бизнес-правило.
- SQL/фильтры/условия, копируемые между репозиториями и отчётами.
- Документация, дублирующая логику «словами» (часто устаревает первой).
- Конфиги, которые повторяют и расходятся с кодом.
Снаружи это выглядит как WET: быстро копипастнули — «потом приведём в порядок» — и через полгода уже никто не знает, какая версия правила правильная.
Когда DRY превращается в проблему
DRY легко довести до абсурда: увидеть два похожих фрагмента и сразу построить абстракцию на будущее. Это уже не DRY, а преждевременная архитектура — и она начинает конфликтовать с KISS/YAGNI.
Практическая граница простая: устраняйте дубли тогда, когда дублируется именно знание, и когда изменение этого знания реально ожидаемо. Иначе вы рискуете получить «умную» абстракцию, которая сложнее исходных двух кусков.
Пример нарушения DRY и рефакторинга (Java)
Представим типичную задачу: есть регистрация пользователя и обновление профиля. В обоих случаях нужно валидировать email и нормализовать его, а также проверять пароль по правилам безопасности. Команда спешит — и копирует правила в два сервиса.
До DRY
public class SignUpService {
public User signUp(String email, String password) {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
String normalizedEmail = email.trim().toLowerCase();
if (password == null || password.length() < 8) {
throw new IllegalArgumentException("Weak password");
}
User user = new User(normalizedEmail);
user.setPasswordHash(hash(password));
return user;
}
private String hash(String raw) {
return Integer.toHexString(raw.hashCode());
}
}
public class ProfileService {
public void changeEmail(User user, String newEmail) {
if (newEmail == null || !newEmail.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
String normalizedEmail = newEmail.trim().toLowerCase();
user.setEmail(normalizedEmail);
}
public void changePassword(User user, String newPassword) {
if (newPassword == null || newPassword.length() < 8) {
throw new IllegalArgumentException("Weak password");
}
user.setPasswordHash(hash(newPassword));
}
private String hash(String raw) {
return Integer.toHexString(raw.hashCode());
}
}
На первый взгляд всё «нормально». Но это WET в чистом виде:
- правила email и пароля продублированы минимум в двух местах;
- хеширование продублировано минимум в двух местах;
- если завтра правило изменится (например, пароль ≥ 12, или email должен быть RFC-валидным), вы обязаны вспомнить про все копии — и почти наверняка не вспомните.
После DRY
Сделаем знание явным: вынесем его в доменные value-object’ы (или хотя бы в отдельный валидатор) и оставим сервисы «оркестраторами».
public final class EmailAddress {
private final String value;
private EmailAddress(String value) {
this.value = value;
}
public static EmailAddress of(String raw) {
if (raw == null) {
throw new IllegalArgumentException("Email is required");
}
String normalized = raw.trim().toLowerCase();
if (!normalized.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
return new EmailAddress(normalized);
}
public String value() {
return value;
}
}
public final class Password {
private final String raw;
private Password(String raw) {
this.raw = raw;
}
public static Password of(String raw) {
if (raw == null || raw.length() < 8) {
throw new IllegalArgumentException("Weak password");
}
return new Password(raw);
}
public String raw() {
return raw;
}
}
public interface PasswordHasher {
String hash(Password password);
}
public class SimplePasswordHasher implements PasswordHasher {
@Override
public String hash(Password password) {
return Integer.toHexString(password.raw().hashCode());
}
}
public class SignUpService {
private final PasswordHasher hasher;
public SignUpService(PasswordHasher hasher) {
this.hasher = hasher;
}
public User signUp(String email, String password) {
EmailAddress emailAddress = EmailAddress.of(email);
Password pwd = Password.of(password);
User user = new User(emailAddress.value());
user.setPasswordHash(hasher.hash(pwd));
return user;
}
}
public class ProfileService {
private final PasswordHasher hasher;
public ProfileService(PasswordHasher hasher) {
this.hasher = hasher;
}
public void changeEmail(User user, String newEmail) {
user.setEmail(EmailAddress.of(newEmail).value());
}
public void changePassword(User user, String newPassword) {
user.setPasswordHash(hasher.hash(Password.of(newPassword)));
}
}
Что изменилось
- Знание централизовано. Правила email/пароля живут в одном месте, а не в двух сервисах. Это и есть «авторитетное представление».
- Сервисы стали проще. Они описывают сценарий, а не содержат правила домена.
- Изменения безопаснее. Любое изменение правила делается один раз.
- Тестирование точнее. Value-object’ы тестируются изолированно, сценарии — отдельно.
- Дублирование перестало быть ловушкой. Больше не нужно помнить, где спрятаны копии.
Заключение
DRY — это дисциплина управления изменениями: если знание меняется, оно должно меняться в одном месте.
WET — это цена отсутствия этой дисциплины: проект начинает жить копиями, а копии неизбежно расходятся.
Хорошее практическое правило: прежде чем дедуплицировать код, спросите себя:
Я устраняю повторение строк — или устраняю повторение знания, которое будет меняться?
Если второе — DRY почти всегда окупится. Если первое — проверьте, не пытаетесь ли вы построить абстракцию раньше времени.