Каталог рефакторингів
Курований каталог технік рефакторингу з книги Мартіна Фаулера Refactoring (2-ге видання). Кожен рефакторинг включає мотивацію, покрокову механіку та приклади.
«Рефакторинг визначається своєю механікою — точною послідовністю кроків, яких ви дотримуєтесь для здійснення зміни.» — Мартін Фаулер
Як використовувати цей каталог
- Виявити запах за допомогою довідника запахів коду
- Знайти відповідний рефакторинг у цьому каталозі
- Дотримуватися механіки крок за кроком
- Тестувати після кожного кроку для збереження поведінки
Золоте правило: Якщо будь-який крок займає більше 10 хвилин, розбийте його на менші кроки.
Найпоширеніші рефакторинги
Extract Method
Коли використовувати: Довгий метод, дубльований код, потрібно назвати концепцію
Мотивація: Перетворити фрагмент коду на метод, назва якого пояснює призначення.
Механіка:
- Створити новий метод, названий за тим, що він робить (не як)
- Скопіювати фрагмент коду в новий метод
- Просканувати локальні змінні, використані у фрагменті
- Передати локальні змінні як параметри (або оголосити в методі)
- Обробити повернені значення належним чином
- Замінити оригінальний фрагмент викликом нового методу
- Тестувати
До:
function printOwing(invoice) {
let outstanding = 0;
console.log("***********************");
console.log("**** Customer Owes ****");
console.log("***********************");
// Обчислення заборгованості
for (const order of invoice.orders) {
outstanding += order.amount;
}
// Друк деталей
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}Після:
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
printDetails(invoice, outstanding);
}
function printBanner() {
console.log("***********************");
console.log("**** Customer Owes ****");
console.log("***********************");
}
function calculateOutstanding(invoice) {
return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}
function printDetails(invoice, outstanding) {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}Inline Method
Коли використовувати: Тіло методу таке ж зрозуміле, як його назва; надмірне делегування
Мотивація: Видалити непотрібну непрямість, коли метод не додає цінності.
Механіка:
- Перевірити, що метод не є поліморфним
- Знайти всі виклики методу
- Замінити кожен виклик тілом методу
- Тестувати після кожної заміни
- Видалити визначення методу
До:
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5;
}Після:
function getRating(driver) {
return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}Extract Variable
Коли використовувати: Складний вираз, важкий для розуміння
Мотивація: Дати назву частині складного виразу.
Механіка:
- Переконатися, що вираз не має побічних ефектів
- Оголосити незмінну змінну
- Встановити її як результат виразу (або частини)
- Замінити оригінальний вираз змінною
- Тестувати
До:
return order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100);Після:
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;Inline Variable
Коли використовувати: Назва змінної не передає більше, ніж вираз
Мотивація: Видалити непотрібну непрямість.
Механіка:
- Перевірити, що права частина не має побічних ефектів
- Якщо змінна не незмінна, зробити її такою і тестувати
- Знайти перше посилання та замінити виразом
- Тестувати
- Повторити для всіх посилань
- Видалити оголошення та присвоєння
- Тестувати
Rename Variable
Коли використовувати: Назва не чітко передає призначення
Мотивація: Хороші назви критичні для чистого коду.
Механіка:
- Якщо змінна широко використовується, розглянути інкапсуляцію
- Знайти всі посилання
- Змінити кожне посилання
- Тестувати
Поради:
- Використовуйте назви, що розкривають намір
- Уникайте абревіатур
- Використовуйте доменну термінологію
// Погано
const d = 30;
const x = users.filter(u => u.a);
// Добре
const daysSinceLastLogin = 30;
const activeUsers = users.filter(user => user.isActive);Change Function Declaration
Коли використовувати: Назва функції не пояснює призначення, параметри потребують зміни
Мотивація: Хороші назви функцій роблять код самодокументованим.
Механіка (проста):
- Видалити непотрібні параметри
- Змінити назву
- Додати потрібні параметри
- Тестувати
Механіка (міграція — для складних змін):
- Якщо видаляєте параметр, переконайтеся, що він не використовується
- Створити нову функцію з бажаним оголошенням
- Зробити так, щоб стара функція викликала нову
- Тестувати
- Змінити виклики на нову функцію
- Тестувати після кожного
- Видалити стару функцію
До:
function circum(radius) {
return 2 * Math.PI * radius;
}Після:
function circumference(radius) {
return 2 * Math.PI * radius;
}Encapsulate Variable
Коли використовувати: Прямий доступ до даних з кількох місць
Мотивація: Забезпечити чітку точку доступу для маніпуляції даними.
Механіка:
- Створити функції getter та setter
- Знайти всі посилання
- Замінити читання на getter
- Замінити записи на setter
- Тестувати після кожної зміни
- Обмежити видимість змінної
До:
let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
// Використовується в багатьох місцях
spaceship.owner = defaultOwner;Після:
let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" };
function defaultOwner() { return defaultOwnerData; }
function setDefaultOwner(arg) { defaultOwnerData = arg; }
spaceship.owner = defaultOwner();Introduce Parameter Object
Коли використовувати: Кілька параметрів, що часто зʼявляються разом
Мотивація: Згрупувати дані, що природно належать разом.
Механіка:
- Створити новий клас/структуру для згрупованих параметрів
- Тестувати
- Використати Change Function Declaration для додавання нового обʼєкта
- Тестувати
- Для кожного параметра в групі видалити його з функції та використати новий обʼєкт
- Тестувати після кожного
До:
function amountInvoiced(startDate, endDate) { ... }
function amountReceived(startDate, endDate) { ... }
function amountOverdue(startDate, endDate) { ... }Після:
class DateRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
}
function amountInvoiced(dateRange) { ... }
function amountReceived(dateRange) { ... }
function amountOverdue(dateRange) { ... }Combine Functions into Class
Коли використовувати: Кілька функцій оперують тими самими даними
Мотивація: Згрупувати функції з даними, над якими вони працюють.
Механіка:
- Застосувати Encapsulate Record до спільних даних
- Перемістити кожну функцію в клас
- Тестувати після кожного переміщення
- Замінити аргументи даних використанням полів класу
До:
function base(reading) { ... }
function taxableCharge(reading) { ... }
function calculateBaseCharge(reading) { ... }Після:
class Reading {
constructor(data) { this._data = data; }
get base() { ... }
get taxableCharge() { ... }
get calculateBaseCharge() { ... }
}Split Phase
Коли використовувати: Код працює з двома різними речами
Мотивація: Розділити код на окремі фази з чіткими межами.
Механіка:
- Створити другу функцію для другої фази
- Тестувати
- Ввести проміжну структуру даних між фазами
- Тестувати
- Витягти першу фазу в окрему функцію
- Тестувати
До:
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;
const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = quantity * shippingPerCase;
return basePrice - discount + shippingCost;
}Після:
function priceOrder(product, quantity, shippingMethod) {
const priceData = calculatePricingData(product, quantity);
return applyShipping(priceData, shippingMethod);
}
function calculatePricingData(product, quantity) {
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;
return { basePrice, quantity, discount };
}
function applyShipping(priceData, shippingMethod) {
const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
return priceData.basePrice - priceData.discount + shippingCost;
}Переміщення функцій (Moving Features)
Move Method
Коли використовувати: Метод використовує більше можливостей іншого класу, ніж свого
Мотивація: Розміщувати функції разом з даними, які вони використовують найбільше.
Механіка:
- Дослідити всі елементи програми, використані методом у своєму класі
- Перевірити, чи метод є поліморфним
- Скопіювати метод у цільовий клас
- Адаптувати до нового контексту
- Зробити оригінальний метод делегуючим до цільового
- Тестувати
- Розглянути видалення оригінального методу
Move Field
Коли використовувати: Поле використовується більше іншим класом
Мотивація: Тримати дані разом з функціями, що їх використовують.
Механіка:
- Інкапсулювати поле, якщо ще не зроблено
- Тестувати
- Створити поле в цільовому класі
- Оновити посилання на використання цільового поля
- Тестувати
- Видалити оригінальне поле
Move Statements into Function
Коли використовувати: Той самий код завжди зʼявляється разом з викликом функції
Мотивація: Видалити дублювання, перемістивши повторюваний код у функцію.
Механіка:
- Витягти повторюваний код у функцію, якщо ще не зроблено
- Перемістити оператори в цю функцію
- Тестувати
- Якщо виклики більше не потребують окремих операторів, видалити їх
Move Statements to Callers
Коли використовувати: Загальна поведінка різниться між викликачами
Мотивація: Коли поведінка має відрізнятися, перемістити її з функції.
Механіка:
- Використати Extract Method на коді для переміщення
- Використати Inline Method на оригінальній функції
- Видалити тепер вбудований виклик
- Перемістити витягнутий код до кожного виклику
- Тестувати
Організація даних (Organizing Data)
Replace Primitive with Object
Коли використовувати: Елемент даних потребує більше поведінки, ніж просте значення
Мотивація: Інкапсулювати дані разом з їхньою поведінкою.
Механіка:
- Застосувати Encapsulate Variable
- Створити простий клас значення
- Змінити setter для створення нового екземпляра
- Змінити getter для повернення значення
- Тестувати
- Додати багатшу поведінку до нового класу
До:
class Order {
constructor(data) {
this.priority = data.priority; // рядок: "high", "rush" тощо
}
}
// Використання
if (order.priority === "high" || order.priority === "rush") { ... }Після:
class Priority {
constructor(value) {
if (!Priority.legalValues().includes(value))
throw new Error(`Invalid priority: ${value}`);
this._value = value;
}
static legalValues() { return ['low', 'normal', 'high', 'rush']; }
get value() { return this._value; }
higherThan(other) {
return Priority.legalValues().indexOf(this._value) >
Priority.legalValues().indexOf(other._value);
}
}
// Використання
if (order.priority.higherThan(new Priority("normal"))) { ... }Replace Temp with Query
Коли використовувати: Тимчасова змінна зберігає результат виразу
Мотивація: Зробити код зрозумілішим, витягнувши вираз у функцію.
Механіка:
- Перевірити, що змінна присвоюється лише один раз
- Витягти праву частину присвоєння в метод
- Замінити посилання на тимчасову змінну викликом методу
- Тестувати
- Видалити оголошення та присвоєння тимчасової змінної
До:
const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}Після:
get basePrice() {
return this._quantity * this._itemPrice;
}
// В методі
if (this.basePrice > 1000) {
return this.basePrice * 0.95;
} else {
return this.basePrice * 0.98;
}Спрощення умовної логіки (Simplifying Conditional Logic)
Decompose Conditional
Коли використовувати: Складний умовний оператор (if-then-else)
Мотивація: Зробити намір зрозумілим, витягнувши умови та дії.
Механіка:
- Застосувати Extract Method на умові
- Застосувати Extract Method на then-гілці
- Застосувати Extract Method на else-гілці (якщо є)
До:
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}Після:
if (isSummer(aDate, plan)) {
charge = summerCharge(quantity, plan);
} else {
charge = regularCharge(quantity, plan);
}
function isSummer(date, plan) {
return !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd);
}
function summerCharge(quantity, plan) {
return quantity * plan.summerRate;
}
function regularCharge(quantity, plan) {
return quantity * plan.regularRate + plan.regularServiceCharge;
}Consolidate Conditional Expression
Коли використовувати: Кілька умов з однаковим результатом
Мотивація: Зробити зрозумілим, що умови є однією перевіркою.
Механіка:
- Перевірити відсутність побічних ефектів в умовах
- Обʼєднати умови за допомогою
andабоor - Розглянути Extract Method на обʼєднаній умові
До:
if (employee.seniority < 2) return 0;
if (employee.monthsDisabled > 12) return 0;
if (employee.isPartTime) return 0;Після:
if (isNotEligibleForDisability(employee)) return 0;
function isNotEligibleForDisability(employee) {
return employee.seniority < 2 ||
employee.monthsDisabled > 12 ||
employee.isPartTime;
}Replace Nested Conditional with Guard Clauses
Коли використовувати: Глибоко вкладені умови ускладнюють відстеження потоку
Мотивація: Використовувати захисні вирази (guard clauses) для спеціальних випадків, зберігаючи нормальний потік зрозумілим.
Механіка:
- Знайти умови спеціальних випадків
- Замінити їх захисними виразами з раннім поверненням
- Тестувати після кожної зміни
До:
function payAmount(employee) {
let result;
if (employee.isSeparated) {
result = { amount: 0, reasonCode: "SEP" };
} else {
if (employee.isRetired) {
result = { amount: 0, reasonCode: "RET" };
} else {
result = calculateNormalPay(employee);
}
}
return result;
}Після:
function payAmount(employee) {
if (employee.isSeparated) return { amount: 0, reasonCode: "SEP" };
if (employee.isRetired) return { amount: 0, reasonCode: "RET" };
return calculateNormalPay(employee);
}Replace Conditional with Polymorphism
Коли використовувати: Switch/case за типом, умовна логіка що варіюється за типом
Мотивація: Дозволити обʼєктам обробляти свою поведінку самостійно.
Механіка:
- Створити ієрархію класів (якщо не існує)
- Використати фабричну функцію для створення обʼєктів
- Перемістити умовну логіку в метод суперкласу
- Створити метод підкласу для кожного випадку
- Видалити оригінальну умову
До:
function plumage(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return "average";
case 'AfricanSwallow':
return (bird.numberOfCoconuts > 2) ? "tired" : "average";
case 'NorwegianBlueParrot':
return (bird.voltage > 100) ? "scorched" : "beautiful";
default:
return "unknown";
}
}Після:
class Bird {
get plumage() { return "unknown"; }
}
class EuropeanSwallow extends Bird {
get plumage() { return "average"; }
}
class AfricanSwallow extends Bird {
get plumage() {
return (this.numberOfCoconuts > 2) ? "tired" : "average";
}
}
class NorwegianBlueParrot extends Bird {
get plumage() {
return (this.voltage > 100) ? "scorched" : "beautiful";
}
}
function createBird(data) {
switch (data.type) {
case 'EuropeanSwallow': return new EuropeanSwallow(data);
case 'AfricanSwallow': return new AfricanSwallow(data);
case 'NorwegianBlueParrot': return new NorwegianBlueParrot(data);
default: return new Bird(data);
}
}Introduce Special Case (Null Object)
Коли використовувати: Повторні перевірки на null для спеціальних випадків
Мотивація: Повертати спеціальний обʼєкт, що обробляє спеціальний випадок.
Механіка:
- Створити клас спеціального випадку з очікуваним інтерфейсом
- Додати перевірку isSpecialCase
- Ввести фабричний метод
- Замінити перевірки на null використанням обʼєкта спеціального випадку
- Тестувати
До:
const customer = site.customer;
// ... багато місць з перевіркою
if (customer === "unknown") {
customerName = "occupant";
} else {
customerName = customer.name;
}Після:
class UnknownCustomer {
get name() { return "occupant"; }
get billingPlan() { return registry.defaultPlan; }
}
// Фабричний метод
function customer(site) {
return site.customer === "unknown"
? new UnknownCustomer()
: site.customer;
}
// Використання — перевірки на null не потрібні
const customerName = customer.name;Рефакторинг API (Refactoring APIs)
Separate Query from Modifier
Коли використовувати: Функція і повертає значення, і має побічні ефекти
Мотивація: Зробити зрозумілим, які операції мають побічні ефекти.
Механіка:
- Створити нову функцію-запит
- Скопіювати логіку повернення оригінальної функції
- Змінити оригінал, щоб повертав void
- Замінити виклики, що використовують повернене значення
- Тестувати
До:
function alertForMiscreant(people) {
for (const p of people) {
if (p === "Don") {
setOffAlarms();
return "Don";
}
if (p === "John") {
setOffAlarms();
return "John";
}
}
return "";
}Після:
function findMiscreant(people) {
for (const p of people) {
if (p === "Don") return "Don";
if (p === "John") return "John";
}
return "";
}
function alertForMiscreant(people) {
if (findMiscreant(people) !== "") setOffAlarms();
}Parameterize Function
Коли використовувати: Кілька функцій роблять схожі речі з різними значеннями
Мотивація: Видалити дублювання додаванням параметра.
Механіка:
- Обрати одну функцію
- Додати параметр для варійованого літерала
- Змінити тіло для використання параметра
- Тестувати
- Змінити виклики на параметризовану версію
- Видалити тепер невикористані функції
До:
function tenPercentRaise(person) {
person.salary = person.salary * 1.10;
}
function fivePercentRaise(person) {
person.salary = person.salary * 1.05;
}Після:
function raise(person, factor) {
person.salary = person.salary * (1 + factor);
}
// Використання
raise(person, 0.10);
raise(person, 0.05);Remove Flag Argument
Коли використовувати: Булевий параметр, що змінює поведінку функції
Мотивація: Зробити поведінку явною через окремі функції.
Механіка:
- Створити явну функцію для кожного значення прапорця
- Замінити кожен виклик відповідною новою функцією
- Тестувати після кожної зміни
- Видалити оригінальну функцію
До:
function bookConcert(customer, isPremium) {
if (isPremium) {
// логіка преміум-бронювання
} else {
// логіка звичайного бронювання
}
}
bookConcert(customer, true);
bookConcert(customer, false);Після:
function bookPremiumConcert(customer) {
// логіка преміум-бронювання
}
function bookRegularConcert(customer) {
// логіка звичайного бронювання
}
bookPremiumConcert(customer);
bookRegularConcert(customer);Робота з успадкуванням (Dealing with Inheritance)
Pull Up Method
Коли використовувати: Той самий метод у кількох підкласах
Мотивація: Видалити дублювання в ієрархії класів.
Механіка:
- Перевірити, що методи ідентичні
- Перевірити, що сигнатури однакові
- Створити новий метод у суперкласі
- Скопіювати тіло з одного підкласу
- Видалити один метод підкласу, тестувати
- Видалити інші методи підкласів, тестувати кожен
Push Down Method
Коли використовувати: Поведінка релевантна лише для підмножини підкласів
Мотивація: Розмістити метод там, де він використовується.
Механіка:
- Скопіювати метод у кожен підклас, що його потребує
- Видалити метод із суперкласу
- Тестувати
- Видалити з підкласів, що його не потребують
- Тестувати
Replace Subclass with Delegate
Коли використовувати: Успадкування використовується некоректно, потрібна більша гнучкість
Мотивація: Віддавати перевагу композиції над успадкуванням, коли доречно.
Механіка:
- Створити порожній клас для делегата
- Додати поле в клас-хост для утримання делегата
- Створити конструктор для делегата, що викликається з хоста
- Перемістити функціональність до делегата
- Тестувати після кожного переміщення
- Замінити успадкування делегуванням
Extract Class
Коли використовувати: Великий клас з кількома відповідальностями
Мотивація: Розділити клас для збереження єдиної відповідальності.
Механіка:
- Вирішити, як розділити відповідальності
- Створити новий клас
- Перемістити поле з оригіналу до нового класу
- Тестувати
- Перемістити методи з оригіналу до нового класу
- Тестувати після кожного переміщення
- Переглянути та перейменувати обидва класи
- Вирішити, як відкрити новий клас
До:
class Person {
get name() { return this._name; }
set name(arg) { this._name = arg; }
get officeAreaCode() { return this._officeAreaCode; }
set officeAreaCode(arg) { this._officeAreaCode = arg; }
get officeNumber() { return this._officeNumber; }
set officeNumber(arg) { this._officeNumber = arg; }
get telephoneNumber() {
return `(${this._officeAreaCode}) ${this._officeNumber}`;
}
}Після:
class Person {
constructor() {
this._telephoneNumber = new TelephoneNumber();
}
get name() { return this._name; }
set name(arg) { this._name = arg; }
get telephoneNumber() { return this._telephoneNumber.toString(); }
get officeAreaCode() { return this._telephoneNumber.areaCode; }
set officeAreaCode(arg) { this._telephoneNumber.areaCode = arg; }
}
class TelephoneNumber {
get areaCode() { return this._areaCode; }
set areaCode(arg) { this._areaCode = arg; }
get number() { return this._number; }
set number(arg) { this._number = arg; }
toString() { return `(${this._areaCode}) ${this._number}`; }
}Короткий довідник: Запах → Рефакторинг
| Запах коду | Основний рефакторинг | Альтернатива |
|---|---|---|
| Long Method | Extract Method | Replace Temp with Query |
| Duplicate Code | Extract Method | Pull Up Method |
| Large Class | Extract Class | Extract Subclass |
| Long Parameter List | Introduce Parameter Object | Preserve Whole Object |
| Feature Envy | Move Method | Extract Method + Move |
| Data Clumps | Extract Class | Introduce Parameter Object |
| Primitive Obsession | Replace Primitive with Object | Replace Type Code |
| Switch Statements | Replace Conditional with Polymorphism | Replace Type Code |
| Temporary Field | Extract Class | Introduce Null Object |
| Message Chains | Hide Delegate | Extract Method |
| Middle Man | Remove Middle Man | Inline Method |
| Divergent Change | Extract Class | Split Phase |
| Shotgun Surgery | Move Method | Inline Class |
| Dead Code | Remove Dead Code | - |
| Speculative Generality | Collapse Hierarchy | Inline Class |
Додаткове читання
- Fowler, M. (2018). Refactoring: Improving the Design of Existing Code (2nd ed.)
- Онлайн-каталог: https://refactoring.com/catalog/

