DRY: Don’t repeat yourself

DRY: Don’t repeat yourself


Makar M.

Введение

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

Report Page