DRY в программировании: как избежать дублирования кода
Принцип "Don't Repeat Yourself" помогает избавиться от повторяющихся фрагментов, упростить поддержку проекта и снизить количество багов.
Если в проекте каждый второй компонент копирует один и тот же кусок логики, возникает вопрос: “Почему я все время пишу одно и то же?”.
Дублирование приводит к росту багов, усложненному тестированию и бесконечным правкам. Разберем, что такое DRY в программировании, почему он важен и как внедрить его без лишних сложностей.
Почему DRY важен для качества кода
Принцип Don’t Repeat Yourself не просто красивая фраза, а фундамент устойчивой архитектуры. Когда одна бизнес-логика реализуется в нескольких местах, ее изменение требует правки в каждом дублированном фрагменте. Шанс пропустить один из них и получить скрытый баг резко возрастает. На практике DRY сокращает объем поддержки и упрощает рефакторинг: меняем только один “источник правды”. Подробнее о влиянии повторения на качество кода можно почитать в статье о рефакторинге.
Как выявить дублирование в проекте
Самый простой способ поиск по репозиторию. Более системный подход включает:
- Анализ git-history: частые коммиты с похожими изменениями указывают на скрытое дублирование.
- Статический анализ: инструменты вроде ESLint или SonarQube отмечают похожие функции.
- Код-ревью: задавайте вопрос “Есть ли уже готовый кусок, который можно переиспользовать?”.
Таблица типичных мест дублирования
Область | Признак дублирования | Как решить |
---|---|---|
UI-компоненты | Похожие шаблоны разметки | Выделить в общий компонент |
Сетевые запросы | Повторяющиеся URL и обработка ошибок | Создать apiClient -модуль |
Валидация форм | Одни и те же правила в разных формах | Выделить в отдельный schema |
Константы | Одни и те же строки/числа в разных файлах | Поместить в constants.js |
Логика бизнес-правил | Дублирование условных веток | Инкапсулировать в сервисный слой |
Пример без дублирования: сервис авторизации
До (дублирование в двух местах):
// auth.js
export function login(email, password) {
return fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
})
.then((r) => r.json())
.then((data) => {
if (data.error) throw new Error(data.error);
localStorage.setItem("token", data.token);
return data;
});
}
// profile.js
export function updateProfile(data) {
return fetch("/api/profile", {
method: "PUT",
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify(data),
})
.then((r) => r.json())
.then((res) => {
if (res.error) throw new Error(res.error);
return res;
});
}
Логика получения токена, обработки ошибок и парсинга JSON повторяется.
После (DRY-решение):
// apiClient.js единый клиент
export async function apiClient(url, options = {}) {
const defaults = {
headers: {
"Content-Type": "application/json",
...(options.auth && {
Authorization: `Bearer ${localStorage.getItem("token")}`,
}),
},
body: options.body ? JSON.stringify(options.body) : undefined,
};
const response = await fetch(url, { ...defaults, ...options });
const data = await response.json();
if (data.error) throw new Error(data.error);
return data;
}
// auth.js
import { apiClient } from "./apiClient";
export function login(email, password) {
return apiClient("/api/login", { method: "POST", body: { email, password } });
}
// profile.js
import { apiClient } from "./apiClient";
export function updateProfile(data) {
return apiClient("/api/profile", { method: "PUT", auth: true, body: data });
}
Весь повторяющийся код сосредоточен в apiClient
. Добавлять новые запросы теперь проще достаточно указать путь и параметры. Похожий подход раскрывается в руководстве по модульному подход.
Как внедрять принцип постепенно
- Идентифицировать “скользкие” места: список функций или компонентов, которые часто меняются одновременно.
- Создать общую абстракцию: отдельный модуль или хук (React) для переноса повторяющегося кода.
- Писать тесты: убедитесь, что после рефакторинга поведение осталось тем же. Тесты ваш страховочный полис, когда меняете “источник правды”.
- Не переусердствовать: если абстракция слишком общая, ее будет трудно понять. Лучше несколько небольших и понятных функций, чем одна огромная.
Ловушки при применении принципа
- Преждевременная оптимизация: создание общих модулей до того, как обнаружено реальное дублирование, приводит к избыточному коду.
- Слишком абстрактные названия: функции вроде
doIt()
ничего не объясняют и только усложняют чтение. - Забытый контекст: одинаковый код в разных частях проекта может зависеть от разных окружений; вынесение в общий модуль может скрыть важные отличия.
- Отсутствие документации: без пояснений новые разработчики могут копировать код заново.
- Игнорирование тестов: без покрывающих тестов изменения в “общем” месте легко сломают несколько функций одновременно.
FAQ / чеклист
- Как проверить, что DRY-принцип соблюден? Прогоните статический анализатор и посмотрите количество дублирующих блоков.
- Можно ли полностью избавиться от дублирования? Нет. Иногда дублирование оправдано, если оно повышает читаемость.
- Что делать, если абстракция стала слишком громоздкой? Разбейте ее на более мелкие части, следуя принципу Single Responsibility.
- Как убедиться, что рефакторинг не сломал бизнес-логику? Пишите короткие юнит-тесты до и после изменений.
- Какие типичные ошибки при вынесении кода в сервисы? Ошибки с контекстом (например,
this
в классах) и забытые зависимости вimport
-ах.
Если ваш проект уже сталкивается с постоянными багами из-за повторяющихся функций, применение DRY поможет собрать все “источники правды” в одном месте и снизить нагрузку на поддержку. Именно в такие моменты принцип экономит время и делает код более предсказуемым.