Skip to main content

DRY (не повторюй себе)

DRY (Don't Repeat Yourself) - кожен фрагмент логіки або знань у системі живе рівно в одному місці.

Теорія

TL;DR

  • Аналогія: один майстер-ключ від усіх дверей замість копій скрізь. Одна копія зламалась - все відкрите.
  • Головна перевага: зміни логіку один раз, і все що від неї залежить оновиться автоматично.
  • WET (Write Everything Twice) змушує оновлювати кілька місць одночасно - звідси розбіжності та баги.
  • Правило: якщо скопіював код або дані - винеси в функцію, константу або модуль.

Швидкий приклад

javascript
// WET: формат імені в двох місцях // Виправив одне, забув інше = прихований баг function userFullName(first, last) { return first + ' ' + last; } function userGreeting(first, last) { return 'Hi, ' + first + ' ' + last + '!'; } // DRY: привітання використовує єдину логіку імені function userFullName(first, last) { return first + ' ' + last; } function userGreeting(first, last) { return 'Hi, ' + userFullName(first, last) + '!'; } // userGreeting('Alice', 'Smith') → 'Hi, Alice Smith!'

У WET-версії форматування імені в двох місцях. Виправив баг в одному, забув про друге. У DRY-версії одне місце для змін.

WET проти DRY

Дублювання коду змушує оновлювати кілька місць одразу. Пропустиш одне - отримаєш розбіжність. Наприклад, правило валідації пароля: 8 символів у формі, але 6 в API-обробнику. DRY централізує логіку: одна функція, одна константа, один модуль. Зміни там - все, що від неї залежить, оновиться автоматично.

Протилежність DRY іноді називають WET (Write Everything Twice). Назва жартівлива лише наполовину.

Коли виносити

  • Копіюєш схожу логіку між функціями або файлами? Витягни в спільну функцію.
  • Одне й те саме рядкове значення або число в кількох місцях? Перенеси в іменовану константу.
  • Однакова валідація у формі, API-обробнику та тесті? Перенеси в спільний утиліт.
  • Повторюваний шаблон API-запитів по всіх маршрутах? Оберни у фабричну функцію.
  • Конфігурація розкидана по файлах? Централізуй в одному конфігураційному об'єкті.

Поведінка рантайму

Компілятори на кшталт V8 або TypeScript виконують кожну копію продубльованої логіки незалежно. Баг у дубльованому коді спрацьовує в кожній копії окремо. Деякі правила ESLint (наприклад, no-dupe-keys) виявляють певні форми дублювання статично, але більшість стає помітною тільки в рантаймі, коли копії починають розходитися.

Типові помилки

Помилка: надмірне застосування DRY до тривіального коду.

javascript
// Безглузда абстракція - збільшує когнітивне навантаження без користі const twiceTwo = multiply(2, 2);

Абстрагуй тільки якщо логіка повторюється 3+ рази або справді складна. Дві схожі функції сьогодні можуть розійтися завтра. Передчасна абстракція ускладнить обидві.

Помилка: дублювання заради «читабельності».

javascript
// WET: додати нову роль означає оновити три місця if (admin || moderator || owner) { ... } // DRY: одне місце для змін const canEdit = ['admin', 'moderator', 'owner'].includes(role);

Баги з правами доступу в продакшені часто виникають через те, що хтось додав роль в одному if, але забув про два інших.

Помилка: ігнорувати дублювання даних.

javascript
// Неправильно: версія API захардкоджена в 5 різних файлах { name: 'API v1', version: '1.0' } // Правильно export const API_VERSION = '1.0';

DRY стосується даних так само, як і коду. Оновлення версії повинне торкатися одного файлу.

Помилка: дві функції з однаковою назвою, але різною логікою в різних модулях.

javascript
// utils.js містить validateEmail() // components/validateEmail.js містить іншу validateEmail()

Імпортуєш не той варіант - отримуєш неочевидні збої. Один модуль, barrel-exports - проблема зникає.

Реальні проєкти

  • React: спільний хук useAuth між сторінками замість повтору логіки авторизації в кожному компоненті (патерн NextAuth.js).
  • Express: фабрика middleware requireRole('admin') замість перевірок ролей inline на кожному маршруті (патерн Passport.js).
  • Node.js: path.join(__dirname, 'config') визначається один раз і імпортується де потрібно.
  • Redux: централізовані action creators, де структура payload живе в одному файлі.
  • TypeScript: спільний інтерфейс User у моделях NestJS замість перевизначення в кожному файлі.

Команди часто DRY-ять код, але залишають URL бази даних захардкодженим у 10 скриптах. Дублювання даних обходиться так само дорого, як і дублювання коду.

Питання на співбесіді

Q: Що є протилежністю DRY?
A: WET (Write Everything Twice або «We Haven't Estimated Anything Totally»). Це код, де однакова логіка живе в кількох місцях.

Q: Коли DRY шкодить більше, ніж допомагає?
A: При передчасній абстракції. Є правило трьох: чекай мінімум трьох повторень перш ніж абстрагувати. Дві схожі функції можуть розійтися завтра, і примусова абстракція ускладнить обидві.

Q: Як DRY пов'язаний з SOLID?
A: DRY підтримує принцип єдиної відповідальності (SRP). Коли логіка живе в одному місці, це місце відповідає за неї і тільки за неї.

Q: Як застосовувати DRY в мікросервісах, не створюючи зв'язності між сервісами?
A: Чисті утиліти (схеми валідації, форматери дат) можна виносити в спільні пакети. Доменну логіку між bounded contexts не варто шарити - це створює зв'язність. Для доменних подій краще використовувати контракти або схеми подій, а не спільний код. Правильна відповідь на рівні senior згадує Domain-Driven Design і визнає, що дублювання між сервісами іноді прийнятне заради їхньої незалежності.

Приклади

Express.js: спільна функція для запитів до БД

javascript
// WET: шаблон запиту повторюється в кожному маршруті app.get('/users', authMiddleware, (req, res) => { db.query('SELECT * FROM users WHERE active=1').then(users => res.json(users)); }); app.get('/orders', authMiddleware, (req, res) => { db.query('SELECT * FROM orders WHERE active=1').then(orders => res.json(orders)); }); // DRY: одна функція для шаблону const fetchActive = (table) => db.query(`SELECT * FROM ${table} WHERE active=1`); app.get('/users', authMiddleware, (req, res) => fetchActive('users').then(res.json.bind(res))); app.get('/orders', authMiddleware, (req, res) => fetchActive('orders').then(res.json.bind(res)));

Додати новий маршрут - один рядок замість копіювання рядка запиту. Умова WHERE active=1 визначена один раз. Змінив на WHERE status='active' - всі маршрути оновились.

React: кастомний хук для повторної логіки запитів

javascript
// WET: логіка запиту скопійована між компонентами function UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers); }, []); } // DRY: винесено в хук для повторного використання function useApiData(url) { const [data, setData] = useState([]); useEffect(() => { fetch(url).then(r => r.json()).then(setData); }, [url]); // url у масиві залежностей - пропустиш це → застарілі дані return data; } const users = useApiData('/api/users'); const products = useApiData('/api/products');

Масив залежностей з url - місце де розробники помиляються найчастіше. Пропустиш його і хук буде запитувати початковий URL назавжди, навіть якщо url зміниться. Один правильно реалізований хук надійніший за п'ять розкиданих блоків useEffect.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?