На початку шляху, коли тільки вчишся програмувати, часто доводиться чути, що не можна повторюватися, треба використовувати принцип DRY. І в якийсь момент студенти дійсно починають вносити кожен повторюваний рядок у функцію і створювати утиліти геть на всі випадки життя. Але чи дійсно це правильний підхід завжди?
Одразу скажу: найчистіший і найідеальніший DRY-код на реальному проєкті дуже важко підтримувати, тому доведеться шукати баланс. Про це ми сьогодні й говоритимемо.
Почнемо з того, що якісний абстрактний код — це чудово, але не коли фікс однієї фічі ламає логіку інших трьох фіч. Обидва принципи: і DRY і WET — це інструменти, які потрібно вміти правильно використовувати.
У цій статті розберемо:
- що таке DRY насправді (і чому це не просто «менше рядків коду»);
- коли варто застосовувати WET;
- типові помилки й антипатерни;
- конкретні приклади з життя;
І головне — як не впасти в крайнощі.
ЩО Ж ТАКЕ DRY?
DRY — Don't Repeat Yourself. І це не означає «ніколи не пиши однаковий код двічі», це означає «вмій розрізнити код, який виконує одну й ту саму функцію, від схожого коду».
Ми почнемо з одного простого прикладу, який показує, коли код повторюваний і як цей самий код має виглядати.
Не DRY:
// Порушення DRY: Знання про ставку податку продубльовано
function calculateTotalPrice(price) {
const tax = price * 0.2; // 20% податок
const total = price + tax;
return total;
}
function generateInvoice(items) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * 0.2; // 20% податок — дублікат!
return {
subtotal,
tax,
total: subtotal + tax
};
}
Що ж тут не так:
- інформація про податкову ставку записана два рази;
- змінювати цю інформацію потрібно буде в 2 різних місцях, які можуть бути більше розкидані по коду, ніж на прикладі, що призводить до того, що одне з цих місць дуже просто забути і загубити.
DRY:
// DRY: Єдине джерело знання про податок
const TAX_RATE = 0.2;
function calculateTax(amount) {
return amount * TAX_RATE;
}
function calculateTotalPrice(price) {
const tax = calculateTax(price);
return price + tax;
}
function generateInvoice(items) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const tax = calculateTax(subtotal);
return {
subtotal,
tax,
total: subtotal + tax
};
}
Що змінилося:
- код простіше читати, редагувати й тестувати;
- тепер шансів забути про одну змінну взагалі немає і відповідно шансів знайти баг, який відноситься до цього, теж стає набагато менше.
Важливе уточнення: іноді може здаватися, що код однаковий і це помилково буде розцінено як «краще це винести».
Дуже поширений приклад такого коду:
// Це НЕ порушення DRY, хоча код схожий
function validateLoginForm(data) {
if (!data.email) return "Email обов'язковий";
if (!data.password) return "Пароль обов'язковий";
if (data.password.length < 8) return "Пароль занадто короткий";
return null;
}
function validateRegistrationForm(data) {
if (!data.email) return "Email обов'язковий";
if (!data.password) return "Пароль обов'язковий";
if (data.password.length < 8) return "Пароль занадто короткий";
if (!data.confirmPassword) return "Підтвердження паролю обов'язкове";
if (data.password !== data.confirmPassword) return "Паролі не співпадають";
return null;
}
Чому це нормально:
- тут різні контексти: логін і реєстрація;
- у майбутньому, коли код буде розширюватися, логіка може доповнюватись і змінюватися;
- це не абстракція, тому читати це легше;
- ну і найбільш прикладне для цього коду — в одному місці може бути 2-факторна перевірка, а в іншому капча.
DRY не змушує «винеси весь проєкт в утиліти».
Є таке поняття, як рання абстракція, і хоч звучить це наче і непогано, але на ділі може викликати проблеми пізніше. Момент, коли потрібно використати DRY пропонується визначати так:
- перший раз ви просто пишете код і ще не знаєте патерн;
- другий раз ви помічаєте випадкову схожість коду (однаковий шаблон обʼєктів User і Product), але іще не абстрагуєте. Це може бути випадковою подібністю;
- якщо вже втретє ви пишете схожий код, то саме тут час подумати про абстракцію.
Як ви вже зрозуміли, існує принцип-протилежність DRY і називається він теж протилежно. Розберімось і з ним теж.
ЩО ТАКЕ WET І ЧОМУ ЦЕ НЕ ЗАВЖДИ ПОГАНО?
WET — Write Everything Twice (або «We Enjoy Typing»).
Це звучить як антипаттерн (але не поспішайте робити висновки). У певному контексті це може бути найкращим рішенням.
Чому? Бо, як ми вже зрозуміли раніше, передчасна абстракція може наробити нам багів при масштабуванні й підтримці.
Коли WET краще:
- ви ще не впевнені, що код справді однаковий;
- дублювання підкреслює різні контексти;
- код читається легше, ніж при абстрагуванні.
До останнього пункту є дуже зрозумілий приклад:
// Погана абстракція
function processData(data, type, config = {}) {
if (type === 'user') {
if (config.includeAvatar) {
return { ...data, avatar: generateAvatar(data.name) };
}
return data;
} else if (type === 'product') {
if (config.includePrice) {
return { ...data, price: calculatePrice(data.basePrice) };
}
return data;
}
}
// Краще — просто і зрозуміло
function processUser(data) {
return { ...data, avatar: generateAvatar(data.name) };
}
function processProduct(data) {
return { ...data, price: calculatePrice(data.basePrice) };
}
Приклади з практики
Шлях від WET до DRY наглядно:
// 📌 ВЕРСІЯ 1: WET — пишемо швидко, розуміємо проблему
function handleLoginSubmit() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
if (!email) {
showError('email', 'Введіть email');
return;
}
if (!password) {
showError('password', 'Введіть пароль');
return;
}
submitLogin({ email, password });
}
function handleRegistrationSubmit() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
const name = document.getElementById('name').value;
if (!email) {
showError('email', 'Введіть email');
return;
}
if (!password) {
showError('password', 'Введіть пароль');
return;
}
if (!name) {
showError('name', 'Введіть ім\'я');
return;
}
submitRegistration({ email, password, name });
}
// 📌 ВЕРСІЯ 2: Помітили паттерн — час рефакторити
function validateField(fieldId, errorMessage) {
const value = document.getElementById(fieldId).value;
if (!value) {
showError(fieldId, errorMessage);
return false;
}
return true;
}
function handleLoginSubmit() {
if (!validateField('email', 'Введіть email')) return;
if (!validateField('password', 'Введіть пароль')) return;
submitLogin({
email: document.getElementById('email').value,
password: document.getElementById('password').value
});
}
function handleRegistrationSubmit() {
if (!validateField('email', 'Введіть email')) return;
if (!validateField('password', 'Введіть пароль')) return;
if (!validateField('name', 'Введіть ім\'я')) return;
submitRegistration({
email: document.getElementById('email').value,
password: document.getElementById('password').value,
name: document.getElementById('name').value
});
}
// 📌 ВЕРСІЯ 3: Фінальна — чиста абстракція
function validateForm(fields) {
return fields.every(({ id, errorMessage }) =>
validateField(id, errorMessage)
);
}
function getFormValues(fieldIds) {
return fieldIds.reduce((acc, id) => {
acc[id] = document.getElementById(id).value;
return acc;
}, {});
}
function handleLoginSubmit() {
const fields = [
{ id: 'email', errorMessage: 'Введіть email' },
{ id: 'password', errorMessage: 'Введіть пароль' }
];
if (!validateForm(fields)) return;
submitLogin(getFormValues(['email', 'password']));
}
function handleRegistrationSubmit() {
const fields = [
{ id: 'email', errorMessage: 'Введіть email' },
{ id: 'password', errorMessage: 'Введіть пароль' },
{ id: 'name', errorMessage: 'Введіть ім\'я' }
];
if (!validateForm(fields)) return;
submitRegistration(getFormValues(['email', 'password', 'name']));
}
Тобто наш шлях такий: написали код вперше, все працює -> помітили повторення, написали невелику абстракцію, все досі працює -> довели до чистоти з розумінням патерну. Важливо, щоб патерн розуміли не тільки ви, а й ваші колеги, які будуть редагувати ваш код пізніше.
РЕКОМЕНДАЦІЇ ВІД СЕБЕ:
Якщо вам досі важко зрозуміти, коли що використовувати, то не хвилюйтесь, у мене є маленька шпаргалка, яка може вам допомогти.
Перед тим, як робити DRY спитайте у себе це:
- Чи я бачив цей самий паттерн більше, ніж в 3-х місцях?
- Чи логіка завжди буде змінюватися синхронно?
- Чи абстракція має просте і зрозуміле API?
- Тести після абстракції залишаються простими?
- Чи код стає легшим для розуміння після абстракції? Чи не стає він складнішим?
- Назва абстракції чітко описує ЩО вона робить.
І якщо у вас більше, ніж 4 ТАК, то можна використати DRY.
Коли ж слід повернутися до WET?
- Додавання нової фічі вимагає змін в 5+ місцях.
- Всередині абстракції є if/else.
- Функція має понад 5 параметрів.
- Ніхто, крім вас самих не розуміє як працює ця абстракція.
- Назва абстракції містить в собі слова по типу «common», «universal», «generic», «helper».
ВИСНОВОК
Не женіться за DRY як за самоціллю. Програмування, як процес — це точно не про те, щоб писати правильний, ідеальний код (існування якого, до речі, вчені досі не довели) з першого разу. Ми тут говоримо про програмування, як про те, щоб постійно адаптувати код під нові вимоги й він працював правильно при цьому.
Не бійтеся повторювати код, це не страшно. Головне завжди розуміти ЧОМУ він повторюється і вміти розрізнити, коли це так і має бути.
Не бійтеся рефакторити, це дає вам можливість побачити нові шляхи для розв'язування проблем.
Хороший код — це той, який легко змінювати і адаптувати, а не той, де менше рядків.