Мистецтво написання простих і коротких функцій

Софт постійно ускладнюється. Стабільність і простота розширення програми безпосередньо залежать від якості коду.
На жаль, майже кожен розробник, і я в тому числі, у своїй роботі стикається з кодом поганої якості. І це — болото. У такого коду є токсичні ознаки:
  • Опції занадто довгі, і на них занадто багато завдань
  • Часто функцій є побічні ефекти, які складно визначити, а іноді навіть складно налагоджувати
  • Незрозумілі імена у функцій і змінних
  • Крихкий код: невелика модифікація несподівано ламає інші компоненти програми
  • Погане покриття коду тестами або взагалі його відсутність
Всім знайомі висловлювання «я не розумію, як працює цей код», «маревний код», «цей код складно змінити» та інші.
Одного разу мій колега звільнився, бо намагався впоратися з REST API на Ruby, який було важко підтримувати. Він отримав цей проект від попередньої команди розробників.
Виправлення поточних помилок створювало нові, додавання нових функцій породжувало нову серію помилок, і так далі (крихкий код). Клієнт не хотів перебудовувати додаток, робити йому зручну структуру, і розробник прийняв правильне рішення — звільнитися.

Такі ситуації трапляються часто, і це сумно. Але що робити?
По-перше, пам'ятати: створити працююче додаток і подбати про якість коду — різні завдання.
З одного боку, ви реалізуєте вимоги програми. Але з іншого, ви повинні витрачати час і перевіряти, чи не висить занадто багато завдань на який-небудь функції, давати змістовні назви змінних і функцій, уникати функцій з побічними ефектами і так далі.
Функції (в тому числі методи об'єкта) — це маленькі шестерні, які змушують додаток працювати. На початку ви повинні зосередитися на їх структуру і склад. Стаття охоплює кращі підходи, як писати прості, зрозумілі і легко досліджувані функції.
1. Функції повинні бути маленькими. Зовсім маленькими.
Уникайте роздутих функцій, у яких дуже багато завдань, краще робити кілька дрібних функцій. Роздуті функції з прихованим змістом важко зрозуміти, модифікувати і, особливо, тестувати.
Уявіть ситуацію, коли функція повинна повертати суму елементів масиву, map'а чи простого об'єкта JavaScript. Сума розраховується складанням значень властивостей:
  • 1 бал за
    null
    або
    undefined
  • 2 бали за примітивний тип
  • 4 бали за об'єкт або функцію
Наприклад, сума масиву
[null, 'Hello World', {}]
обчислюється так:
1
(
null
) +
2
(за рядок, примітивний тип) +
4
(за об'єкт) =
7
.
Крок 0: Первинна велика функція
Давайте почнемо з гіршого методу. Ідея — писати код однією великою функцією
getCollectionWeight()
:
function getCollectionWeight(collection) { 
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = [...collection.values()];
} else {
collectionValues = Object.keys(collection).map(function (key) {
return collection[key];
});
}
return collectionValues.reduce(function sum, item) {
if (item == null) {
return sum + 1;
} 
if (typeof item === 'object' || typeof item === 'function') {
return sum + 4;
}
return sum + 2;
}, 0);
}
let myArray = [null, { }, 15]; 
let myMap = new Map([ ['functionKey', function() {}] ]); 
let myObject = { 'stringKey': 'Hello world' }; 
getCollectionWeight(myArray); // => 7 (1 + 4 + 2) 
getCollectionWeight(myMap); // => 4 
getCollectionWeight(myObject); // => 2 

Проблема добре видно. Функція
getCollectionWeight()
занадто роздута і виглядає як чорний ящик, повний сюрпризів.
Ви, швидше за все, з першого погляду складно зрозуміти, яка в неї завдання. А уявіть набір таких функцій у додатку.
Коли ви працюєте з таким кодом, ви растрачиваете час і зусилля. А якісний код не викличе у вас дискомфорту. Якісний код з короткими і не вимагають пояснення функціями приємно читати і легко підтримувати.

Крок 1: Витягаємо вага за типом і ліквідуємо магічні числа
Тепер мета — розбити довгу функцію на дрібні, незалежні і переиспользуемые. Перший крок — отримати код, який визначає суму значення за його типом. Ця нова функція буде називатися
getWeight()
.
Також зверніть увагу на магічні цифри цієї суми:
1
,
2
та
4
. Просто читання цих цифр, без розуміння всієї історії, не дає корисної інформації. На щастя, ES2015 дозволяє оголосити
const
read-only, так що можна легко створювати константи зі значущими іменами і ліквідувати магічні числа.
Давайте створимо невелику функцію
getWeightByType()
і одночасно вдосконалимо
getCollectionWeight()
:
// Code extracted into getWeightByType()
function getWeightByType(value) { 
const WEIGHT_NULL_UNDEFINED = 1;
const WEIGHT_PRIMITIVE = 2;
const WEIGHT_OBJECT_FUNCTION = 4;
if (value == null) {
return WEIGHT_NULL_UNDEFINED;
} 
if (typeof value === 'object' || typeof value === 'function') {
return WEIGHT_OBJECT_FUNCTION;
}
return WEIGHT_PRIMITIVE;
}
function getCollectionWeight(collection) { 
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = [...collection.values()];
} else {
collectionValues = Object.keys(collection).map(function (key) {
return collection[key];
});
}
return collectionValues.reduce(function sum, item) {
return sum + getWeightByType(item);
}, 0);
}
let myArray = [null, { }, 15]; 
let myMap = new Map([ ['functionKey', function() {}] ]); 
let myObject = { 'stringKey': 'Hello world' }; 
getCollectionWeight(myArray); // => 7 (1 + 4 + 2) 
getCollectionWeight(myMap); // => 4 
getCollectionWeight(myObject); // => 2 

Правда, виглядає краще?
Функція
getWeightByType()
— незалежний компонент, який просто визначає суму за типом. І вона переиспользуемая, тому що може виконуватися в межах будь-якої іншої функції.
getCollectionWeight()
стає трохи більш полегшеної
WEIGHT_NULL_UNDEFINED
,
WEIGHT_PRIMITIVE
та
WEIGHT_OBJECT_FUNCTION
— не потребують пояснення константи, які описують типи сум. Вам не потрібно здогадуватися, що означають цифри
1
,
2
та
4
.
Крок 2: Продовжуємо поділ і робимо функції расширяемыми
Оновлена версія, як і раніше володіє недоліками.
Уявіть собі, що у вас є план реалізувати порівняння значень Set або взагалі інший довільній колекції.
getCollectionWeight()
буде швидко збільшуватися в розмірах, так як її логіка — збирати значення.
винесімо код, який збирає значення map
getMapValues()
і простих JavaScript-об'єктів
getPlainObjectValues()
в окремі функції. Подивіться на поліпшену версію:
function getWeightByType(value) { 
const WEIGHT_NULL_UNDEFINED = 1;
const WEIGHT_PRIMITIVE = 2;
const WEIGHT_OBJECT_FUNCTION = 4;
if (value == null) {
return WEIGHT_NULL_UNDEFINED;
} 
if (typeof value === 'object' || typeof value === 'function') {
return WEIGHT_OBJECT_FUNCTION;
}
return WEIGHT_PRIMITIVE;
}
// Code extracted into getMapValues()
function getMapValues(map) { 
return [...map.values()];
}
// Code extracted into getPlainObjectValues()
function getPlainObjectValues(object) { 
return Object.keys(object).map(function (key) {
return object[key];
});
}
function getCollectionWeight(collection) { 
let collectionValues;
if (collection instanceof Array) {
collectionValues = collection;
} else if (collection instanceof Map) {
collectionValues = getMapValues(collection);
} else {
collectionValues = getPlainObjectValues(collection);
}
return collectionValues.reduce(function sum, item) {
return sum + getWeightByType(item);
}, 0);
}
let myArray = [null, { }, 15]; 
let myMap = new Map([ ['functionKey', function() {}] ]); 
let myObject = { 'stringKey': 'Hello world' }; 
getCollectionWeight(myArray); // => 7 (1 + 4 + 2) 
getCollectionWeight(myMap); // => 4 
getCollectionWeight(myObject); // => 2 

Зараз читаючи
getCollectionWeight()
вам набагато простіше зрозуміти, що ця функція робить. Виглядає, як цікава історія.
Кожна функція очевидна і дохідлива. Ви не витрачаєте час, намагаючись зрозуміти, що робить такий код. Ось наскільки чистим він повинен бути.
Крок 3: Ніколи не припиняйте поліпшення
Навіть на цьому етапі у вас є багато можливостей для підвищення якості!
Ви можете створити окрему
getCollectionValues()
, яка містить оператори if/else і диференціює типи колекцій:
function getCollectionValues(collection) { 
if (collection instanceof Array) {
return collection;
}
if (collection instanceof Map) {
return getMapValues(collection);
}
return getPlainObjectValues(collection);
}

Тоді
getCollectionWeight()
стане дійсно простий, тому що єдине, що потрібно зробити, це отримати значення колекції
getCollectionValues()
і застосувати до нього sum reducer.
також Можна створити окрему функцію скорочення:
function reduceWeightSum(sum, item) { 
return sum + getWeightByType(item);
}

Тому що в ідеалі
getCollectionWeight()
не повинна визначати функції.
зрештою початкова велика функція перетворюється в маленькі:
function getWeightByType(value) { 
const WEIGHT_NULL_UNDEFINED = 1;
const WEIGHT_PRIMITIVE = 2;
const WEIGHT_OBJECT_FUNCTION = 4;
if (value == null) {
return WEIGHT_NULL_UNDEFINED;
} 
if (typeof value === 'object' || typeof value === 'function') {
return WEIGHT_OBJECT_FUNCTION;
}
return WEIGHT_PRIMITIVE;
}
function getMapValues(map) { 
return [...map.values()];
}
function getPlainObjectValues(object) { 
return Object.keys(object).map(function (key) {
return object[key];
});
}
function getCollectionValues(collection) { 
if (collection instanceof Array) {
return collection;
}
if (collection instanceof Map) {
return getMapValues(collection);
}
return getPlainObjectValues(collection);
}
function reduceWeightSum(sum, item) { 
return sum + getWeightByType(item);
}
function getCollectionWeight(collection) { 
return getCollectionValues(collection).reduce(reduceWeightSum, 0);
}
let myArray = [null, { }, 15]; 
let myMap = new Map([ ['functionKey', function() {}] ]); 
let myObject = { 'stringKey': 'Hello world' }; 
getCollectionWeight(myArray); // => 7 (1 + 4 + 2) 
getCollectionWeight(myMap); // => 4 
getCollectionWeight(myObject); // => 2 

Це мистецтво створення невеликих і простих функцій!
Після всіх оптимізацій якості коду з'являється жменю недурных переваг:
  • Читаність
    getCollectionWeight()
    спростилося завдяки не потребує пояснення коду
  • Розмір
    getCollectionWeight()
    значно зменшився
  • Функція
    getCollectionWeight()
    тепер захищена від швидкого розростання, якщо ви захочете реалізувати роботу з іншими типами колекцій
  • Витягнуті функції тепер — це разгруппированные і переиспользуемые компоненти. Ваш колега може попросити вас імпортувати ці приємні функції в інший проект, і ви зможете це легко зробити.
  • Якщо випадково функція згенерує помилку, стек викликів буде більш точним, оскільки містить імена функцій. Майже відразу можна виявити функцію, яка створює проблеми.
  • Розділені функції набагато простіше тестувати і досягати високого рівня покриття коду тестами. Замість того, щоб тестувати одну роздуту функцію усіма можливими сценаріями, ви можете структурувати тести і перевіряти кожну маленьку функцію окремо.
  • Можна використовувати формат модулів CommonJS або ES2015. Створювати окремі модулі з витягнутих функцій. Це зробить файли вашого проекту легкими і структурованими.
Ці переваги допоможуть вам вижити в складній структурі додатків.

Загальне правило — функції не повинні бути більше 20 рядків коду. Чим менше, тим краще.
Я думаю, тепер у вас з'явиться справедливе питання: «Я не хочу створювати функції для кожного рядка коду. Є якісь критерії, коли потрібно зупинитися?» Це тема наступного розділу.
2. Функції повинні бути простими
Давайте трохи відвернемося і подумаємо, що таке програма?
Кожна програма реалізує набір вимог. Завдання розробника — розділити ці вимоги на невеликі виконувані компоненти (області видимості, класи, функції, блоки коду), які виконують чітко визначені операції.
Компонент складається з інших більш дрібних компонентів. Якщо ви хочете написати код для компонента, його потрібно створювати з компонентів тільки попереднього рівня абстракції.
Іншими словами, потрібно розкласти функцію на більш дрібні кроки, але всі вони повинні знаходиться на одному, попередньому, рівні абстракції. Важливо це тому, що функція стає простою і передбачає "виконання однієї задачі, і виконання це — якісне".
У чому необхідність? Прості функції — очевидні. Очевидність означає легке читання і модифікацію.
Спробуємо наслідувати приклад. Припустимо, ви хочете реалізувати функцію, яка зберігає тільки прості числа (2, 3, 5, 7, 11, і т. д.) масиву і видаляє інші (1, 4, 6, 8 тощо). Функція викликається так:
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11] 

Які кроки попереднього рівня абстракції потрібні для реалізації функції
getOnlyPrime()
? Давайте сформулюємо так:
Для реалізації
getOnlyPrime()
фільтрувати масив чисел за допомогою функції
IsPrime()
.
Просто застосуйте функцію-фільтр
IsPrime()
до масиву.
Є необхідність на цьому рівні реалізувати деталі
IsPrime()
? Ні, тому що тоді у функції
getOnlyPrime()
з'являться кроки з іншого рівня абстракцій. Функція прийме на себе занадто багато завдань.
Не забуваючи цю просту ідею, давайте реалізуємо тіло функції
getOnlyPrime()
:
function getOnlyPrime(numbers) { 
return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11] 

Як бачите,
getOnlyPrime()
— елементарна функція. Вона містить кроки з одного рівня абстракції: метод
.filter()
масиву та
IsPrime()
.
Тепер прийшов час перейти на попередній рівень абстракції.
Метод масиву
.filter()
входить в JavaScript і використовується як є. Звичайно, стандарт описує саме те, що виконує метод.
Тепер можна конкретизувати те, як буде реалізована
IsPrime()
:
Щоб реалізувати функцію
IsPrime()
, яка перевіряє, чи є число n є простим, потрібно перевірити, чи ділиться n на будь-яке число від
2
Math.sqrt(n)
без залишку.
Давайте напишемо код для функції
IsPrime()
, користуючись цим алгоритмом (він ще не ефективний, я використовував його для простоти):
function isPrime(number) { 
if (number === 3 || number === 2) {
return true;
}
if (number === 1) {
return false;
}
for (let divisor = 2; divisor <= Math.sqrt(number); divisor++) {
if (number % divisor === 0) {
return false;
}
}
return true;
}
function getOnlyPrime(numbers) { 
return numbers.filter(isPrime);
}
getOnlyPrime([2, 3, 4, 5, 6, 8, 11]); // => [2, 3, 5, 11] 

getOnlyPrime()
— маленька і елементарна. В ній тільки строго необхідні кроки попереднього рівня абстракції.
Читання складних функцій може бути значно спрощено, якщо слідувати правилу робити їх очевидними. Якщо код кожного рівня абстракції написаний педантично, це запобіжить породження великих шматків незручного коду.
3. Використовуйте компактні назви функцій
Імена функцій повинні бути компактними: не більше і не менше. В ідеалі назва повинна чітко вказувати, що робить функція, без необхідності ритися в деталях реалізації.
Для імен функцій використовуйте формат camel case, який починається з маленької літери:
addItem()
,
saveToStore()
або
getFirstName()
.
Оскільки функція — це дія, що її ім'я має містити, як мінімум, один дієслово. Наприклад
deletePage()
,
verifyCredentials()
. Щоб отримати або встановити властивість, використовуйте префікси set і get:
getLastName()
або
setLastName()
.
У production уникайте заплутують імена, начебто
Foo()
,
bar()
,
а()
,
fun()
і подібні. Такі імена не мають сенсу.
Якщо функції маленькі і прості, а імена компактні: код читається як хороша книга.
4. Висновок
Звичайно, наведені приклади прості. Додатки, що існують в реальності, більш складні. Можна скаржитися, що писати прості функції попереднього рівня абстракції — нудне заняття. Але воно не настільки трудомістке якщо робити це з самого початку проекту.
Якщо у програмі вже є занадто роздуті функції, перебудувати код, швидше за все, буде складно. І в багатьох випадках неможливо в розумних часових проміжках. Почніть хоча б з малого: витягніть те, що зможете.
Звичайно, правильне рішення — грамотно реалізувати програму з самого початку. І вкласти час не тільки в реалізацію, але і в правильну структуру функцій: зробити їх маленькими і простими.
Сім разів відміряй, один раз відріж.

У ES2015 реалізована хороша модульна система, яка чітко показує, що невеликі функції — це хороша практика.
Просто пам'ятайте, що чистий і організований код завжди вимагає вкладень часу. Вам може бути складно. Вам, можливо, буде потрібно довго практикуватися. Ви можете повертатися і змінювати функції по кілька разів.
Немає нічого гірше брудного коду.
Які методи використовуєте ви, щоб зробити код організованим?
(Переклад Наталії Басс)
Джерело: Хабрахабр

0 коментарів

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