Рефакторинг салону відеопрокату на JavaScript

Моя книга по рефакторінгу в 1999 році починалася з простого прикладу розрахунку і форматування чека для відеомагазинах. На сучасному JavaScript є кілька варіантів рефакторінгу того коду. Тут я викладу чотири з них: рефакторинг функцій верхнього рівня; перехід до вкладеної функції з диспетчером; використовуючи класи; трансформація із застосуванням проміжної структури даних.

Багато років тому, коли я писав книгу по рефакторінгу, я почав з (дуже) простого прикладу рефакторінгу коду, який обчислював рахунок для клієнта за прокат відеофільмів (в ті дні нам потрібно було ходити в салон для цього). Нещодавно я розмірковував про це прикладі, зокрема, як би він виглядав на сучасному JavaScript.

Будь рефакторинг передбачає поліпшення коду в певному напрямку, в тому, яка відповідає стилю програмування команди розробників. Приклад у книзі на Java, а Java (саме в той час) передбачала певний стиль програмування, об'єктно-орієнтований стиль. Однак з JavaScript є набагато більше варіантів, який стиль вибрати. Хоча ви можете дотримуватися Java-подібного об'єктно-орієнтованого стилю, особливо з ES6 (Ecmascript 2015), не всі прихильники JavaScript схвалюють цей стиль. Багато хто дійсно вважають, що використовувати класи Дуже Погано.

Початковий код салону відеопрокату
Щоб продовжити пояснення, потрібно показати певний код. У цьому випадку JavaScript-версію початкового прикладу, який я написав в кінці минулого століття.

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
let movie = movies[r.movieID];
let thisAmount = 0;

// determine amount for each movie
switch (movie.code) {
case "regular":
thisAmount = 2;
if (r.days > 2) {
thisAmount += (r.days - 2) * 1.5;
}
break;
case "new":
thisAmount = r.days * 3;
break;
case "childrens":
thisAmount = 1.5;
if (r.days > 3) {
thisAmount += (r.days - 3) * 1.5;
}
break;
}

//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if(movie.code === "new" && r.days > 2) frequentRenterPoints++;

//print figures for this rental
result += `\t${movie.title}\t{thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;
}

Тут я використовую ES6. Код працює на двох структурах даних, обидві з яких представляють собою просто списки записів json. Запис клієнта виглядає так:

{
"name": "martin",
"rentals": [
{"movieID": "F001", "days": 3},
{"movieID": "F002", "days": 1},
]
}

Структура списку фільмів виглядає наступним чином:

{
"F001": {"title": "Ran", "code": "regular"},
"F002": {"title": "Trois Couleurs: Bleu", "code": "regular"},
// etc
}
В оригінальній книзі фільми були представлені як об'єкти в структурі об'єктів Java. Для цієї статті я вирішив перейти на структуру json. Передбачається, що якийсь вид глобального пошуку на зразок Repository не підходить для цього додатка

Метод видає просте текстове повідомлення про прокаті відеофільму.

Rental Record for martin
 
Ran 3.5
 
Trois Couleurs: Bleu 2
 
Amount owed is 5.5
 
You earned 2 frequent renter points

Така видача досить груба, навіть для прикладу. Як я міг не попрацювати хоча б пристойно відформатувати цифри? Але пам'ятайте, що книга була написана в часи Java 1.1, до додавання в мову формату String. Це частково виправдовує мою лінь

Функція
statement
пахне як Long Method. Один лише її розмір вже наводить на підозри. Але один поганий запах — не причина для рефакторінгу. Погано факторизованный код є проблемою, тому що його важко зрозуміти. Якщо код важко зрозуміти, то його важко змінити, щоб додати нові функції або виправити помилки. Так що якщо вам не потрібно читати чи розуміти якийсь код, то його погана структура ніяк не зашкодить вам і ви з радістю залишите його в спокої на деякий час. Тому, щоб пробудити у нас інтерес до цього коду, повинна бути якась причина для його зміни. Наша причина, яку я вказав в книзі, — це створення HTML-версії звіту
statement
приблизно з такою видачею:

<h1>Rental Record for <em>martin</em></h1>
<table>
<tr><td>Ran</td><td>3.5</td></tr>
<tr><td>Trois Couleurs: Bleu</td><td>2</td></tr>
</table>
<p>Amount owed is <em>5.5</em></p>
<p>You earned <em>2</em> frequent renter points</p>

Як я зазначив раніше, в цій статті я вивчаю деякі способи рефакторінгу коду, щоб спростити додавання додаткових варіантів візуалізації видачі. Всі вони починаються однаково: розбити один метод на набір функцій, що охоплюють різні частини логіки. Коли я закінчу це розбиття, то вивчу чотири способи, як ці функції можна організувати для підтримки альтернативних видів візуалізації.


Розбиття на кілька функцій
Кожен раз, коли я працюю з дуже довгою функцією кшталт цієї, моя перша думка — спробувати розбити її на логічні шматки коду і зробити з них окремі функції з допомогою Extract Method. [1]. Перший шматок, який привернув мою увагу, — це оператор
switch
.

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
let movie = movies[r.movieID];
let thisAmount = 0;

// determine amount for each movie
switch (movie.code) {
case "regular":
thisAmount = 2;
if (r.days > 2) {
thisAmount += (r.days - 2) * 1.5;
}
break;
case "new":
thisAmount = r.days * 3;
break;
case "childrens":
thisAmount = 1.5;
if (r.days > 3) {
thisAmount += (r.days - 3) * 1.5;
}
break;
}

//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if(movie.code === "new" && r.days > 2) frequentRenterPoints++;

//print figures for this rental
result += `\t${movie.title}\t{thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;
}

Моя середовище розробки (IntelliJ) пропонує сама зробити рефакторинг автоматично, але некоректно проводить його — її здатності JavaScript не настільки розвинені, як у рефакторинге Java. Так що я зроблю це вручну. Потрібно подивитися, які дані використовує цей кандидат на вилучення. Там три фрагмента даних:

  • thisAmount
    обчислюється витягнутим кодом. Я можу ініціювати його всередині функції і повернути в кінці.
  • r
    на кількість днів прокату перевіряється в циклі, я можу передати його як параметр.
  • Мінлива
    movie
    — це фільм, який ви взяли напрокат. Тимчасові змінні, як це зазвичай заважають під час рефакторінгу процедурного коду, так що я вважатиму за краще спочатку запустити Replace Temp with Query для перетворення її в функцію, яку можу викликати з будь-якого витягнутого коду.
Коли я закінчив з Replace Temp with Query, код виглядає так:

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
let thisAmount = 0;

// determine amount for each movie
switch (movieFor®.code) {
case "regular":
thisAmount = 2;
if (r.days > 2) {
thisAmount += (r.days - 2) * 1.5;
}
break;
case "new":
thisAmount = r.days * 3;
break;
case "childrens":
thisAmount = 1.5;
if (r.days > 3) {
thisAmount += (r.days - 3) * 1.5;
}
break;
}

//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if(movieFor®.code === "new" && r.days > 2) frequentRenterPoints++;

//print figures for this rental
result += `\t${movieFor®.title}\t{thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;

function movieFor(rental) {return movies[rental.movieID];}
}

Тепер витягуємо оператор
switch
.

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;

for (let r of customer.rentals) {
const thisAmount = amountFor®;

//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if(movieFor®.code === "new" && r.days > 2) frequentRenterPoints++;

//print figures for this rental
result += `\t${movieFor®.title}\t{thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;

function movieFor(rental) {return movies[rental.movieID];}

function amountFor® {
let thisAmount = 0;

// determine amount for each movie
switch (movieFor®.code) {
case "regular":
thisAmount = 2;
if (r.days > 2) {
thisAmount += (r.days - 2) * 1.5;
}
break;
case "new":
thisAmount = r.days * 3;
break;
case "childrens":
thisAmount = 1.5;
if (r.days > 3) {
thisAmount += (r.days - 3) * 1.5;
}
break;
}
return thisAmount;
}
}

Тепер подивимося на обчислення балів постійного клієнта. Тут можна зробити таку ж процедуру вилучення.

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
const thisAmount = amountFor®;
frequentRenterPointsFor®;

//print figures for this rental
result += `\t${movieFor®.title}\t{thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;
...
function frequentRenterPointsFor® {
//add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if (movieFor®.code === "new" && r.days > 2) frequentRenterPoints++;
}

Хоча я витягнув функцію, але мені не подобається, як вона працює, оновлюючи змінну батьківського області. Такі побічні ефекти ускладнюють код, так що я його зміню його, щоб позбавити цих побічних ефектів.

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
const thisAmount = amountFor®;
frequentRenterPoints += frequentRenterPointsFor®;

//print figures for this rental
result += `\t${movieFor®.title}\t{thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;
...
function frequentRenterPointsFor® {
let result = 1;
if (movieFor®.code === "new" && r.days > 2) result++;
return result;
}

Скористаюся шансом злегка почистити дві витягнуті функції, поки я їх розумію.

function amountFor(rental) {
let result = 0;
switch (movieFor(rental).code) {
case "regular":
result = 2;
if (rental.days > 2) {
result += (rental.days - 2) * 1.5;
}
return result;
case "new":
result = rental.days * 3;
return result;
case "childrens":
result = 1.5;
if (rental.days > 3) {
result += (rental.days - 3) * 1.5;
}
return result;
}
return result;
}
function frequentRenterPointsFor(rental) {
return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
}
З цими функціями я міг би зробити щось ще, особливо з
amountFor
, і я дійсно дещо зробив у книзі. Але для цієї статті я більше не буду заглиблюватися в дослідження тіла цих функцій


Готово, тепер повертаюся до тіла функції.

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
const thisAmount = amountFor®;
frequentRenterPoints += frequentRenterPointsFor®;

//print figures for this rental
result += `\t${movieFor®.title}\t{thisAmount}\n` ;
totalAmount += thisAmount;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;

Загальний підхід, який я люблю використовувати, полягає в усуненні mutable-змінних. Тут їх три, одна збирає фінальну рядок, ще дві обчислюють значення, які використовуються в цій рядку. Не маю нічого проти першої, але хотілося б знищити дві інші. Для початку слід розбити цикл. Спочатку спрощуємо цикл і вбудовуємо постійну величину.

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
frequentRenterPoints += frequentRenterPointsFor®;
result += `\t${movieFor®.title}\t{amountFor®}\n` ;
totalAmount += amountFor®;
}
// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;

Потім розбиваємо цикл на три частини.

function statement(customer, movies) {
let totalAmount = 0;
let frequentRenterPoints = 0;
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
frequentRenterPoints += frequentRenterPointsFor®;
}
for (let r of customer.rentals) {
result += `\t${movieFor®.title}\t{amountFor®}\n`;
}
for (let r of customer.rentals) {
totalAmount += amountFor®;
}

// add footer lines
result += `Amount owed is ${totalAmount}\n`;
result += `You earned ${frequentRenterPoints} frequent renter points\n`;

return result;
Деякі програмісти турбуються про проблеми з продуктивністю після такого рефакторінгу, в такому випадку подивіться стару, але доречну статтю про програмної продуктивності

Таке розбиття дозволяє потім витягти функції для цих обчислень.

function statement(customer, movies) {
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${movieFor®.title}\t{amountFor®}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;

function totalAmount() {
let result = 0;
for (let r of customer.rentals) {
result += amountFor®;
}
return result;
}
function totalFrequentRenterPoints() {
let result = 0;
for (let r of customer.rentals) {
result += frequentRenterPointsFor®;
}
return result;
}

Як фанат ланцюжків послідовного збору даних типу collection pipeline я також отрегулирую цикли в такий ланцюжок.

function totalFrequentRenterPoints() {
return customer.rentals
.map(® => frequentRenterPointsFor®)
.reduce((a, b) => a + b)
;
}
function totalAmount() {
return customer.rentals
.reduce((total, r) => total + amountFor®, 0);
}
Не впевнений, який з цих двох типів ланцюжків мені більше подобається

Дослідження скомпонованої функції
Тепер подивимося, що у нас вийшло. Ось весь код.

function statement(customer, movies) {
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${movieFor®.title}\t{amountFor®}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;

function totalFrequentRenterPoints() {
return customer.rentals
.map(® => frequentRenterPointsFor®)
.reduce((a, b) => a + b)
;
}
function totalAmount() {
return customer.rentals
.reduce((total, r) => total + amountFor®, 0);
}
function movieFor(rental) {
return movies[rental.movieID];
}
function amountFor(rental) {
let result = 0;
switch (movieFor(rental).code) {
case "regular":
result = 2;
if (rental.days > 2) {
result += (rental.days - 2) * 1.5;
}
return result;
case "new":
result = rental.days * 3;
return result;
case "childrens":
result = 1.5;
if (rental.days > 3) {
result += (rental.days - 3) * 1.5;
}
return result;
}
return result;
}
function frequentRenterPointsFor(rental) {
return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
}
}

Тепер у мене добре скомпонована функція. Основний її код займає сім рядків, і всі вони відносяться до форматування підсумкового рядка. Код для всіх обчислень перенесено в свій набір вкладених функцій, кожна з яких невелика і з виразною назвою, яке показує її призначення.

Але я все ще не готовий писати функцію для видачі html. Всі функції після розбиття вкладені в межах загальної функції
statement
. Так легше витягувати функції, так як вони можуть посилатися на імена всередині області видимості функції, в тому числі один на одного (як
amountFor
викликає
movieFor
), та відповідні параметри
customer
та
movie
. Але я не можу написати просту функцію
htmlStatement
, яка посилається на ці функції. Щоб підтримувати якісь інші формати видачі з використанням тих же обчислень, потрібно продовжити рефакторинг. Тепер я дстиг точки, коли з'являються різні варіанти рефакторінгу в залежності від того, як я хочу перетворити код. Далі я випробую кожен з цих варіантів, поясню, як працює кожен з них, а коли всі чотири будуть готові, ми порівняємо їх.

Використання параметра для визначення видачі
Один з варіантів — визначити формат видачі як аргумент функції
statement
. Я хотів би почати такий рефакторинг з використання Add Parameter, витягти існуючий код для форматування тексту і дописати код на початку для відсилання до витягнутої функції, коли параметр вказує на це.

function statement(customer, movies, format = 'text') {
switch (format) {
case "text":
return textStatement();
}
throw new Error(`unknown statement format ${date}`);
function textStatement() {
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${movieFor®.title}\t{amountFor®}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;
}

Потім я можу написати функцію генерації html і додати умову для диспетчера.

function statement(customer, movies, format = 'text') {
switch (format) {
case "text":
return textStatement();
case "html":
return htmlStatement();
}
throw new Error(`unknown statement format ${date}`);

function htmlStatement() {
let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
result += "<table>\n";
for (let r of customer.rentals) {
result + = ` <tr><td>${movieFor®.title}</td><td>${amountFor®}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>Amount owed is <em>${totalAmount()}</em></p>\n`;
result += `<p>You earned <em>${totalFrequentRenterPoints()}</em> frequent renter points</p>\n`;
return result;
}

Я можу використовувати структуру даних для логіки диспетчера.

function statement(customer, movies, format = 'text') {
const dispatchTable = {
"text": textStatement,
"html": htmlStatement
};
if (undefined === dispatchTable[format]) throw new Error(`unknown statement format ${date}`);
return dispatchTable[format].call();


Використання функцій верхнього рівня
Проблема з написанням функції
statement
верхнього рівня полягає в тому, що функції обчислення вкладені всередину функції
statement
. Очевидним виходом буде перенести їх у верхній контекст.

Щоб зробити це, я почав з пошуку функції, яка не посилається ні на які інші, в нашому випадку це
movieFor
.

Всякий раз, коли я переміщаю функції, мені подобається спочатку скопіювати функцію в новий контекст, вмонтувати її в цей контекст, а потім замінити тіло оригінальної функції викликом на переміщення функцію.

function topMovieFor(rental, movies) {
return movies[rental.movieID];
}
function statement(customer, movies) {
// [snip]
function movieFor(rental) {
return topMovieFor(rental, movies);
}
function frequentRenterPointsFor(rental) {
return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
}

На цьому етапі можна компілювати і тестувати код, щоб перевірити, якщо виникнуть якісь проблеми із-за зміни контексту. Коли це зроблено, можна вбудувати функцію переадресації.

function movieFor(rental, movies) {
return movies[rental.movieID];
}
function statement(customer, movies) {
// [snip]
function frequentRenterPointsFor(rental) {
return (movieFor(rental, movies).code === "new" && rental.days > 2) ? 2 : 1;
}
Схожі зміни зроблені всередині
amountFor


Одночасно з вбудовуванням я також перейменував функцію верхнього рівня для відповідності старим імені, так що єдиною відмінністю зараз залишився параметр
movies
.

Потім зробимо це з усіма вкладеними функціями.

function statement(customer, movies) {
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${movieFor(r, movies).title}\t{amountFor(r, movies)}\n`;
}
result += `Amount owed is ${totalAmount(customer, movies)}\n`;
result += `You earned ${totalFrequentRenterPoints(customer, movies)} frequent renter points\n`;
return result;
}
function totalFrequentRenterPoints(customer, movies) {
return customer.rentals
.map(® => frequentRenterPointsFor(r, movies))
.reduce((a, b) => a + b)
;
}
function totalAmount(customer, movies){
return customer.rentals
.reduce((total, r) => total + amountFor(r, movies), 0);
}
function movieFor(rental, movies) {
return movies[rental.movieID];
}
function amountFor(rental, movies) {
let result = 0;
switch (movieFor(rental, movies).code) {
case "regular":
result = 2;
if (rental.days > 2) {
result += (rental.days - 2) * 1.5;
}
return result;
case "new":
result = rental.days * 3;
return result;
case "childrens":
result = 1.5;
if (rental.days > 3) {
result += (rental.days - 3) * 1.5;
}
return result;
}
return result;
}
function frequentRenterPointsFor(rental, movies) {
return (movieFor(rental, movies).code === "new" && rental.days > 2) ? 2 : 1;
}

Тепер я можу легко написати функцію
htmlStatement
.

function htmlStatement(customer, movies) {
let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
result += "<table>\n";
for (let r of customer.rentals) {
result += ` <tr><td>${movieFor(r, movies).title}</td><td>${amountFor(r, movies)}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>Amount owed is <em>${totalAmount(customer, movies)}</em></p>\n`;
result += `<p>You earned <em>${totalFrequentRenterPoints(customer, movies)}</em> frequent renter points</p>\n`;
return result;
}

Оголошення деяких локальних функцій з частковим застосуванням
Коли глобальна функція використовується таким чином, списки параметрів можуть досить сильно розтягнутися. Так що іноді може бути корисно оголосити локальну функцію, яка викликає глобальну функцію з деякими або всіма параметрами всередині. Ця локальна функція, яка є частковим застосуванням глобальної функції, може стати в нагоді для подальшого використання. Є різні способи зробити таке в JavaScript. Один з них — присвоїти локальні функції змінним.

function htmlStatement(customer, movies) {
const amount = () => totalAmount(customer, movies);
const frequentRenterPoints = () => totalFrequentRenterPoints(customer, movies);
const movie = (aRental) => movieFor(aRental, movies);
const rentalAmount = (aRental) => amountFor(aRental, movies);
let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
result += "<table>\n";
for (let r of customer.rentals) {
result + = ` <tr><td>${movie®.title}</td><td>${rentalAmount®}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>Amount owed is <em>${amount()}</em></p>\n`;
result += `<p>You earned <em>${frequentRenterPoints()}</em> frequent renter points</p>\n`;
return result;
}

Інший спосіб — оголосити їх як вкладені функції.

function htmlStatement(customer, movies) {
let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
result += "<table>\n";
for (let r of customer.rentals) {
result + = ` <tr><td>${movie®.title}</td><td>${rentalAmount®}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>Amount owed is <em>${amount()}</em></p>\n`;
result += `<p>You earned <em>${frequentRenterPoints()}</em> frequent renter points</p>\n`;
return result;

function amount() {return totalAmount(customer, movies);}
function frequentRenterPoints() {return totalFrequentRenterPoints(customer, movies);}
function rentalAmount(aRental) {return amountFor(aRental, movies);}
function movie(aRental) {return movieFor(aRental, movies);}
}

Ще один варіант — використовувати
bind
. Залишу вам його для власних вишукувань — це не те, що я б використовував тут, оскільки попередні варіанти мені здаються більш придатними.

Використання класів
Мені знайомий саме об'єктний підхід, так що не дивно, що я збираюся розглянути класи і об'єкти. У ES6 з'явився хороший синтаксис для класичного об'єктного підходу. Подивимося, як застосувати його в цьому прикладі.

Першим ділом обернем дані в об'єкти, почавши з
customer
.

customer.es6...
export default class Customer {
constructor(data) {
this._data = data;
}

get name() {return this._data.name;}
get rentals() { return this._data.rentals;}
}

statement.es6...
import Customer from './customer.es6';

function statement(customerArg, movies) {
const customer = new Customer(customerArg);
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${movieFor®.title}\t{amountFor®}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;

Досі клас є простий обгорткою навколо оригінального об'єкта JavaScript. Далі зробимо таку ж
rental
.

rental.es6...
export default class Rental {
constructor(data) {
this._data = data;
}
get days() {return this._data.days}
get movieID() {return this._data.movieID}
}

customer.es6...
import Rental from './rental.es6'

export default class Customer {
constructor(data) {
this._data = data;
}

get name() {return this._data.name;}
get rentals() { return this._data.rentals.map(r => new Rental®);}
}

Тепер, коли класи створені навколо моїх простих об'єктів json, з'явилася робота для Move Method. Як і під час перенесення функцій на верхній рівень, насамперед візьмемо ту функцію, яка не звертається ні до яких інших —
movieFor
. Але цієї функції потрібен список фільмів в якості контексту, який потрібно буде зробити доступним для створюваних об'єктів
rental
.

statement.es6...
function statement(customerArg, movies) {
const customer = new Customer(customerArg, movies);
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${movieFor®.title}\t{amountFor®}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;

class Customer...
constructor(data, movies) {
this._data = data;
this._movies = movies
}
get rentals() { return this._data.rentals.map(r => new Rental(r, this._movies));}

class Rental...
constructor(data, movies) {
this._data = data;
this._movies = movies;
}

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

statement.es6...
function movieFor(rental) {
return rental.movie;
}

class Rental...
class Rental...
get movie() {
return this._movies[this.movieID];
}

Як і з попереднім переміщенням, насамперед перенесемо ключове поведінку функції в новий контекст, вмонтуємо його в контекст, і налаштуємо оригінальну функцію для виклику нової функції. Коли це працює, відносно просто вмонтувати виклики оригінальної функції.

statement.es6...
function statement(customerArg, movies) {
const customer = new Customer(customerArg, movies);
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${r.movie.title}\t{amountFor®}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;
function amountFor(rental) {
let result = 0;
switch (rental.movie.code) {
case "regular":
result = 2;
if (rental.days > 2) {
result += (rental.days - 2) * 1.5;
}
return result;
case "new":
result = rental.days * 3;
return result;
case "childrens":
result = 1.5;
if (rental.days > 3){
result += (rental.days - 3) * 1.5;
}
return result;
}
return result;
}
function frequentRenterPointsFor(rental) {
return (rental.movie.code === "new" && rental.days > 2) ? 2 : 1;
}

Можна використовувати ту саму базову послідовність для переміщення у
rental
і двох обчислень.

statement.es6...
function statement(customerArg, movies) {
const customer = new Customer(customerArg, movies);
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${r.movie.title}\t{r.amount}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;
function totalFrequentRenterPoints() {
return customer.rentals
.map(® => r.frequentRenterPoints)
.reduce((a, b) => a + b)
;
}
function totalAmount() {
return customer.rentals
.reduce((total, r) => total + r.amount, 0);
}

class Rental...
get frequentRenterPoints() {
return (this.movie.code === "new" && this.days > 2) ? 2 : 1;
}
get amount() {
let result = 0;
switch (this.movie.code) {
case "regular":
result = 2;
if (this.days > 2) {
result += (this.days - 2) * 1.5;
}
return result;
case "new":
result = this.days * 3;
return result;
case "childrens":
result = 1.5;
if (this.days > 3) {
result += (this.days - 3) * 1.5;
}
return result;
}
return result;
}

Потім я можу перемістити до
customer
дві функції обчислення суми.

statement.es6...
function statement(customerArg, movies) {
const customer = new Customer(customerArg, movies);
let result = `Rental Record for ${customer.name}\n`;
for (let r of customer.rentals) {
result += `\t${r.movie.title}\t{r.amount}\n`;
}
result += `Amount owed is ${customer.amount}\n`;
result += `You earned ${customer.frequentRenterPoints} frequent renter points\n`;
return result;
}

class Customer...
get frequentRenterPoints() {
return this.rentals
.map(® => r.frequentRenterPoints)
.reduce((a, b) => a + b)
;
}
get amount() {
return this.rentals
.reduce((total, r) => total + r.amount, 0);
}

Коли логіка обчислень перемістилася в об'єкти
rental
customer
, написати html-версію
statement
просто.

statement.es6...
function htmlStatement(customerArg, movies) {
const customer = new Customer(customerArg, movies);
let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
result += "<table>\n";
for (let r of customer.rentals) {
result += ` <tr><td>${r.movie.title}</td><td>${r.amount}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>Amount owed is <em>${customer.amount}</em></p>\n`;
result += `<p>You earned <em>${customer.frequentRenterPoints}</em> frequent renter points</p>\n`;
return result;
}

Класи без синтаксису
Синтаксис класів у ES2015 суперечливий, а деякі думають, що він взагалі не потрібен (косо поглядаючи на Java-розробників). Ви можете проробити ті ж етапи рефакторінгу і отримати результати зразок таких:

function statement(customerArg, movies) {
const customer = createCustomer(customerArg, movies);
let result = `Rental Record for ${customer.name()}\n`;
for (let r of customer.rentals()) {
result += `\t${r.movie().title}\t{r.amount()}\n`;
}
result += `Amount owed is ${customer.amount()}\n`;
result += `You earned ${customer.frequentRenterPoints()} frequent renter points\n`;
return result;
}

function createCustomer(data, movies) {
return {
name: () => data.name,
rentals: rentals,
amount: amount,
frequentRenterPoints: frequentRenterPoints
};

function rentals() {
return data.rentals.map(r => createRental(r, movies));
}
function frequentRenterPoints() {
return rentals()
.map(® => r.frequentRenterPoints())
.reduce((a, b) => a + b)
;
}
function amount() {
return rentals()
.reduce((total, r) => total + r.amount(), 0);
}
}

function createRental(data, movies) {
return {
days: () => data.days,
movieID: () => data.movieID,
movie: movie,
amount: amount,
frequentRenterPoints: frequentRenterPoints
};

function movie() {
return movies[data.movieID];
}

function amount() {
let result = 0;
switch (movie().code) {
case "regular":
result = 2;
if (data.days > 2) {
result += (data.days - 2) * 1.5;
}
return result;
case "new":
result = data.days * 3;
return result;
case "childrens":
result = 1.5;
if (data.days > 3) {
result += (data.days - 3) * 1.5;
}
return result;
}
return result;
}

function frequentRenterPoints() {
return (movie().code === "new" && data.days > 2) ? 2 : 1;
}

У цьому підході використовується шаблон Function As Object. Функції-конструктори (
createCustomer
та
createRental
) повертають об'єкт JavaScript (хеш) викликів функції. Кожна функція-конструктор містить замикання з даними об'єкта. Оскільки об'єкти повертаються функції знаходяться в тому ж контексті функції, вони мають доступ до цих даних. З моєї точки зору це такий же шаблон, що і використання синтаксису класів, але реалізований інакше. Я віддаю перевагу використовувати явний синтаксис, тому що він більш очевидний — це дозволяє мені ясніше міркувати.

Перетворення даних
Всі ці підходи передбачають, що функції друку
statement
викликають інші функції для обчислення потрібних даних. Це можна зробити інакше: передати ці дані функції друку звіту в самій структурі даних. При такому підході функції обчислення використовуються для перетворення структури даних
customer
таким чином, що вона буде містити всі дані, необхідні функції друку.

У термінах рефакторінгу це приклад ще не написаного рефакторінгу Split Phase, який описав мені Кент Бек минулого літа. З таким рефакторінгом я розбиваю обчислення на дві фази, які сполучаються між собою через проміжну структуру даних. Почнемо цей рефакторинг з введення проміжної структури даних.

function statement(customer, movies) {
const data = createStatementData(customer, movies);
let result = `Rental Record for ${data.name}\n`;
for (let r of data.rentals) {
result += `\t${movieFor®.title}\t{amountFor®}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;

function createStatementData(customer, movies) {
let result = Object.assign({}, customer);
return result;
}

Для цього випадку я поліпшу оригінальну структуру даних
customer
, додавши в неї елементи, почавши з виклику до
Object.assign
. Я міг би зробити і повністю нову структуру даних. В реальності вибір залежить від того, наскільки відрізняється перетворена структура даних від оригінальної.

Потім те ж саме зробимо з кожним рядком
rental
.

function statement...
function createStatementData(customer, movies) {
let result = Object.assign({}, customer);
result.rentals = customer.rentals.map(r => createRentalData®);
return result;

function createRentalData(rental) {
let result = Object.assign({}, rental);
return result;
}
}

Зверніть увагу, що я вмонтував
createRentalData
всередину
createStatementData
, оскільки для будь-якого виклику
createStatementData
не потрібно знати, як все влаштовано всередині.

Потім можна приступити до заповнення перетворених даних, почавши з назв фільмів, виданих напрокат.

function statement(customer, movies) {
const data = createStatementData(customer, movies);
let result = `Rental Record for ${data.name}\n`;
for (let r of data.rentals){
result += `\t${r.title}\t{amountFor®}\n`;
}
result += `Amount owed is ${totalAmount()}\n`;
result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
return result;
//...

function createStatementData(customer, movies) {
// ...
function createRentalData(rental) {
let result = Object.assign({}, rental);
result.title = movieFor(rental).title;
return result;
}
}

Продовжимо з обчисленням кількості і загальної суми.

function statement(customer, movies) {
const data = createStatementData(customer, movies);
let result = `Rental Record for ${data.name}\n`;
for (let r of data.rentals) {
result += `\t${r.title}\t{r.amount}\n`;
}
result += `Amount owed is ${data.totalAmount}\n`;
result += `You earned ${data.totalFrequentRenterPoints} frequent renter points\n`;
return result;

function createStatementData(customer, movies) {
let result = Object.assign({}, customer);
result.rentals = customer.rentals.map(r => createRentalData®);
result.totalAmount = totalAmount();
result.totalFrequentRenterPoints = totalFrequentRenterPoints();
return result;

function createRentalData(rental) {
let result = Object.assign({}, rental);
result.title = movieFor(rental).title;
result.amount = amountFor(rental);
return result;
}
}

Тепер, коли всі обчислювальні функції викладають результат своїх обчислень у вигляді даних, можна перенести функції, відокремивши їх від функції рендеринга
statement
. Спочатку я перенесу все обчислювальні функції всередину
createStatementData
.

function statement (customer, movies) {
// body ...
function createStatementData (customer, movies) {
// body ...

function createRentalData(rental) { ... }
function totalFrequentRenterPoints() { ... }
function totalAmount() { ... }
function movieFor(rental) { ... }
function amountFor(rental) { ... }
function frequentRenterPointsFor(rental) { ... }
}
}

Потім перенесу
createStatementData
за межі
statement
.

function statement (customer, movies) { ... }

function createStatementData (customer, movies) {
function createRentalData(rental) { ... }
function totalFrequentRenterPoints() { ... }
function totalAmount() { ... }
function movieFor(rental) { ... }
function amountFor(rental) { ... }
function frequentRenterPointsFor(rental) { ... }
}

Коли я розділив функції таким чином, можна написати HTML-версію
statement
, яка буде використовувати ту ж структуру даних.

function htmlStatement(customer, movies) {
const data = createStatementData(customer, movies);
let result = `<h1>Rental Record for <em>${data.name}</em></h1>\n`;
result += "<table>\n";
for (let r of data.rentals) {
result += ` <tr><td>${r.title}</td><td>${r.amount}</td></tr>\n`;
}
result += "</table>\n";
result += `<p>Amount owed is <em>${data.totalAmount}</em></p>\n`;
result += `<p>You earned <em>${data.totalFrequentRenterPoints}</em> frequent renter points</p>\n`;
return result;
}

Можна також перенести
createStatementData
в окремий модуль, щоб ще чіткіше позначити межі між обчисленням даних і візуалізації (печаткою) звітів.

Порівняння підходів
Отже, настав час відступити назад і окинути поглядом результат. Початковий код, написаний як єдина вбудована функція. Я захотів провести рефакторинг коду, щоб зробити HTML-рендеринг звіту без повторення коду обчислень. Першим ділом я розбив цей код на кілька функцій, які існують всередині оригінальної функції. Після цього я досліджував чотири окремих шляху.




top-level-functions

всі функції пишемо як функції верхнього рівня

function htmlStatement(customer, movies)
function textStatement(customer, movies)
function totalAmount(customer, movies)
function totalFrequentRenterPoints(customer, movies)
function amountFor(rental, movies)
function frequentRenterPointsFor(rental, movies)
function movieFor(rental, movies)
показати код


parameter-dispatch

використовуємо параметр функції верхнього рівня для затвердження формату видачі

function statement(customer, movies, format)
function htmlStatement()
function textStatement()
function totalAmount()
function totalFrequentRenterPoints()
function amountFor(rental)
function frequentRenterPointsFor(rental)
function movieFor(rental)
показати код


classes

переносимо логіку обчислень в класи, які використовуються функціями візуалізації

function textStatement(customer, movies)
function htmlStatement(customer, movies)
class Customer
get amount()
get frequentRenterPoints()
get rentals()
class Rental
get amount()
get frequentRenterPoints()
get movie()
показати код


transform

ділимо логіку обчислення на окремі вкладені функції, які виробляють проміжну структуру даних для функцій візуалізації

function statement(customer, movies)
function htmlStatement(customer, movies)
function createStatementData(customer, movies)
function createRentalData()
function totalAmount()
function totalFrequentRenterPoints()
function amountFor(rental)
function frequentRenterPointsFor(rental)
function movieFor(rental)
показати код


Почну з прикладу функцій верхнього рівня, щоб вибрати в якості базису для порівняння концептуально просту альтернативу. [2] Вона проста, тому що ділить роботу на ряд чистих функцій, а до них усіх можна звернутися з будь-якої точки коду. Таке просто використовувати і просто тестувати — я можу легко протестувати будь-яку окрему функцію або за допомогою наборів тестових даних, або з допомогою REPL.

Негативна сторона
top-level-functions
— у великій кількості повторюваних передач параметрів. Кожній функції потрібно дати структуру даних з фільмами, а функціям рівня
customer
— ще і структуру даних користувачів. Мене тут хвилює не набір одного і того ж тексту на клавіатурі, а читання одного й того ж. Кожен раз при читанні параметрів я повинен зрозуміти, що це таке, і перевірити їх на зміну. Для всіх цих функцій дані про користувачів і фільмах є загальним контекстом — але з функціями верхнього рівня цей загальний контекст не виділено явно. Я роблю такий висновок, коли читаю програму і строю модель її виконання в своїй голові, і я волію, щоб речі були настільки виразними, наскільки можливо.

Цей аргумент стає більш важливим у міру збільшення обсягу контексту. Тут у мене тільки два елемента даних, але нерідко зустрічається і більша кількість. Використовуючи тільки функції верхнього рівня, можна сильно збільшити списки параметрів, які містять всі контексти для багатьох функцій, і в кінці кінців втратити розуміння, що ці функції роблять. Я можу зменшити біль від того, що відбувається, визначивши локальні функції з частковим застосуванням, але тоді потрібно кинути в коктейль ще багато додаткових функцій — які доведеться продублювати з кожним бітом клієнтського коду.

Перевага трьох інших альтернатив в тому, що кожна з них робить явним загальний контекст, захоплюючи його в структуру програми. Підхід
parameter-dispatch
робить це, захоплюючи контекст у списку параметрів верхнього рівня, який потім доступний як загальний контекст для всіх вкладених функцій. Це особливо добре працює з оригінальним кодом, роблячи рефакторинг єдиної функції у вкладені функції простіше, ніж в мові, в якому відсутні вкладені функції.

Але підхід
parameter-dispatch
починає вихлять, коли від мого контексту потрібні інші загальні моделі поведінки, зразок відповіді у форматі HTML. Мені довелося написати щось на зразок диспетчера для визначення, яку функцію я хочу викликати. Визначення формату для рендерера — це не дуже погано, але така логіка диспетчера явно тхне. Однак я написав її, вона все ще істотно копіює базову можливість мови за викликом іменованої функції. І я йду по шляху, який швидко призводить мене до такої безглуздості:

function executeFunction (name, args) {
const dispatchTable = {
//...

Для такого підходу є контекст, якщо вибір формату видачі надходить на виклик дані. В цьому випадку повинен бути механізм диспетчера для цього елемента даних. Однак якщо виклик йде до функції
statement
зразок такого…

const someValue = statement(customer, movieList, 'text');

… то я взагалі не можу написати жодну логіку диспетчера в коді.

Ключове тут — спосіб виклику. Використовувати литеральные значення для вказівки вибору функції — поганий підхід. Замість цього API дозволимо викликає оператору сказати, що він хоче отримати, в самій назві функції,
textStatement
або
htmlStatement
. Тоді я можу використовувати механізм диспетчера функцій у мові та уникнути нагромадження милиць своїми силами.

Отже, з двома варіантами в кишені, де я перебуваю? Я хочу якийсь явний загальний контекст для якоїсь логіки, але з допомогою цієї логіки потрібно викликати різні операції. Коли доводиться стикатися з подібними вимогами, мені негайно приходить думка про об'єктному підході — який по суті представляє набір незалежно викликаються операцій у загальному контексті. [3] Це приводить мене до прикладу з класами, в якому можна захопити загальний контекст користувачів і фільмів в об'єктах
customer
та
rental
. Я встановлюю контекст один раз, коли стверджую об'єкти, а потім вся подальша логіка може використовувати загальний контекст.

Об'єктні методи схожі на локальні функції з частковим застосуванням у першому прикладі, за винятком того, що тут загальний контекст забезпечується конструктором. Тому я пишу тільки локальні функції, а не функції верхнього рівня. Викликає оператор вказує контекст з конструктором, а потім викликає локальну функцію безпосередньо. Я можу уявити локальні методи як варіант часткового застосування абстрактних функцій верхнього рівня на загальному контексті примірника об'єкта.

Використання класів вводить додаткове поняття — відділення логіки рендеринга від логіки обчислень. Один з недоліків первісної єдиної функції в тому, що вона змішує їх разом. Розриви функції в якійсь мірі розділяє їх, але вони як і раніше існують в єдиному концептуальному просторі. Це трохи неправильно, я б міг помістити обчислювальні функції в один файл, а функції рендеринга в інший, зв'язати їх відповідними операторами імпорту. Але мені здається, що загальний контекст сам по собі дає підказку, як згрупувати логіку по модулям.

Я описав об'єкти як загальний набір з частковим застосуванням, але на них можна подивитися інакше. Об'єкти створюються з вхідною структурою даних, але поповнення цих даних результатами обчислень відбувається через їх обчислювальні функції. Я підсилив такий погляд на речі, зробивши ці геттери, так що клієнт розцінює їх в точності так само, як сирі дані — застосовуючи Uniform Access Principle. На це можна подивитися як на перехід від аргументу конструктора до цієї віртуальної структурі даних геттеров. У прикладі перетворення
transform
та ж ідея, але реалізована створенням нової структури даних, яка поєднує початкові дані і результати обчислень. Як об'єкти інкапсулюють обчислювальну логіку всередину класів
customer
та
rental
, так само і підхід
transform
інкапсулює цю логіку всередину
createStatementData
та
createRentalData
. Цей підхід перетворення базових структур даних List And Hash часто є загальною властивістю функціонального мислення. Він дозволяє функцій
create...Data
розділяти потрібний їм контекст, а логікою візуалізації використовувати різноманітну видачу без ускладнень.

Одна невелика відмінність між уявним поданням класів як перетворення і підходом
transform
проявляється, коли відбувається розрахунок перетворення. У підході
transform
тут перетворюється всі відразу, в той час як класи здійснюють індивідуальні перетворення з кожним викликом. Я можу легко перейти від одного підходу до іншого, коли один розрахунок збігається з іншим. У випадку з класами я можу виконати всі розрахунки за раз в конструкторі. У випадку з
transform
я можу зробити перерахунок на вимогу, повернувши функції в проміжну структуру даних. Майже завжди різниця в продуктивності тут буде незначною. Якщо якісь із цих функцій вимагають багато ресурсів, то зазвичай першим ділом я використовую метод/функцію і кэширую результат після першого дзвінка.
Отже, чотири підходу — який віддати перевагу? Мені не хочеться писати логіку диспетчера, так що я б не використовував підхід
parameter-dispatch
. Можна було б вибрати
top-level-functions
, але вони швидко псують враження про себе, коли загальний контекст збільшується в розмірі. Навіть з двома аргументами я б краще вмонтував функції для досягнення інших альтернатив. Вибрати між класами і перетворенням важче, обидва підходи дають можливість зробити явним загальний контекст і поділ. Мені нецікаві півнячі бої, так що я просто кину монетку і хай вибере жереб победиителя.

Подальший рефакторинг
У цьому дослідженні я вивчив чотири способи організації функцій для обчислень і візуалізації. Програмний код — дуже податлива середовище, так що існують і інші варіанти, але ці чотири я вважаю найбільш цікавими для обговорення.

Крім організації функцій, можливий і подальший рефакторинг. У прикладі в книзі я ще розбиваю обчислення
amount
та
frequentRenterPoint
для розширення моделі новими типами фільмів. У коді візуалізації я б теж зробив деякі зміни, наприклад, витягнув би загальний шаблон заголовків, рядків і футера. Але я думаю, що чотирьох варіантів рефакторінгу достатньо для роздумів в цій статті.

В якості висновку, якби він у мене був, я сказав би, що існують різні способи розумно організувати з вигляду однакові обчислення. Різні мови підштовхують до певних стилів — рефакторинг в оригінальній книзі був зроблений на Java, що явно підштовхує до використання класів. JavaScript вправно підтримує різноманіття стилів. Це добре, тому що надає програмісту варіанти, і погано з тієї ж причини — тому що надає програмісту варіанти. (Одна зі складностей програмування на JavaScript у відсутності єдиної думки, що вважати гарним стилем). Корисно знати різні стилі, але більш важливо розуміти, що зв'язує їх разом. Маленькі функції з правильними іменами можуть поєднуватися один з одним і взаємодіяти, забезпечуючи різні потреби як в один час, так і в майбутньому. Загальні контексти пропонують згрупувати логіку разом, в той час як мистецтво програмування полягає у визначенні, як їх розділити в ясний набір таких контекстів.

Примітки
[1] Цей каталог рефакторінгу був написаний в часи популярності об'єктно-орієнтованого словника, так що я використовував термін «метод» для найменування функції/підпрограми/процедури тощо. В JavaScript буде більш розумно використовувати термін «функція», але я використовую назви термінів рефакторінгу з книги. Повернутися до статті

[2] Варіант
parameter-dispatch
краще підходить для початкового рефакторінгу, оскільки його структура ближче до оригінального набору вкладених функцій. Але якщо порівнювати альтернативи, тоді простіше почати з
top-level-functions
. Повернутися до статті

[3] Мені швидше подобається пропозиція Уілла Кука для визначення об'єкта «. Повернутися до статті

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.