Чистий javascript.Функції

Переклад книги Райана Макдермота clean-code-javascript.
Зміст:


Аргументи функції (ідеально 2 або менше)

Обмеження кількості параметрів функції неймовірно важливо, оскільки воно спрощує тестування функції. Наявність більш ніж трьох аргументів призводить до комбінаторного вибуху, коли вам доводиться перебирати масу різних випадків з кожним окремим аргументом.
Ідеальна ситуація — відсутність аргументів. Один або два аргументи — добре, а три вже слід уникати.
Більша кількість аргументів необхідно консолідувати. Як правило, якщо передається більше двох аргументів, ваша функція намагається зробити дуже багато. У тих випадках, коли це все ж не так, краще використовувати об'єкт як аргумент. Оскільки JavaScript дозволяє створювати об'єкти «на льоту», без спеціального опису класів, їх цілком можна застосовувати, коли потрібно передати безліч аргументів.
Погано:
function createMenu(title, body, buttonText, cancellable) {
// ...
}

Добре:
const конфігураційного меню = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
};

function createMenu(config) {
// ...
}

createMenu(конфігураційного меню);


Функція повинна вирішувати одне завдання

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

Погано:
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}

Добре:
function emailClients(clients) {
clients
.filter(isClientActive)
.forEach(email);
}

function isClientActive(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}

Назви функцій повинні описувати їх призначення

Погано:
function addToDate(date, month) {
// ...
}

const date = new Date();

// Імені функції важко сказати, що саме додається
addToDate(date, 1);

Добре:
function addMonthToDate(month, date) {
// ...
}

const date = new Date();
addMonthToDate(1, date);

Функції повинні представляти тільки один рівень абстракції

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

Погано:
function parseBetterJSAlternative(code) {
const REGEXES = [
// ...
];

const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
});
});

const ast = [];
tokens.forEach((token) => {
// правило...
});

ast.forEach((node) => {
// парсинг...
});
}

Добре:
function tokenize(code) {
const REGEXES = [
// ...
];

const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
tokens.push( /* ... */ );
});
});

return tokens;
}

function lexer(tokens) {
const ast = [];
tokens.forEach((token) => {
ast.push( /* ... */ );
});

return ast;
}

function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
ast.forEach((node) => {
// парсинг...
});
}

Позбавляйтеся від дублювання коду

Щосили намагайтеся уникати дублювання коду. Дубльований код шкідливий тим, що має на увазі наявність більш ніж одного місця, в яке доведеться вносити правки, якщо логіка дій зміниться.
Уявіть, що керуєте рестораном і ведете облік всіх продуктів — помідорів, цибулі, часнику, спецій і т. д. Якщо їх облік ведеться в різних списках, то подача будь-якої страви з помідорами вимагає внесення змін у кожен список. Якщо ж список тільки один, то і правка буде всього одна!
Найчастіше дубльований код виникає в тих випадках, коли потрібно реалізувати два або більше розрізняються незначно дії, які в цілому дуже схожі, але їх відмінності змушують вас завести дві чи більше функції, що роблять практично одне і те ж. В цьому випадку позбавлення від дублювання коду буде означати створення абстракції, яка зможе представити всі відмінності у вигляді однієї функції, класу або модуля.
Створення правильної абстракції — питання надзвичайної важливості, і саме тому ви повинні слідувати принципам SOLID. Погані абстракції можуть виявитися гірше дубльованого коду, так що будьте обережні!
Підводячи підсумок: якщо можете обернути код хорошою абстракцією — так і зробіть! Не дублюйте код, інакше вам доведеться вносити безліч правок на кожне невелика зміна.
Погано:
function showDeveloperList(developers) {
developers.forEach((developer) => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};

render(data);
});
}

function showManagerList(managers) {
managers.forEach((manager) => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};

render(data);
});
}

Добре:
function showList(employees) {
employees.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();

let portfolio = employee.getGithubLink();

if (employee.type === 'manager') {
portfolio = employee.getMBAProjects();
}

const data = {
expectedSalary,
experience,
portfolio
};

render(data);
});
}

Встановлюйте об'єкти за замовчуванням з помощию Object.assign

Погано:
const конфігураційного меню = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
};

function createMenu(config) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz';
config.cancellable = config.cancellable === undefined ? config.cancellable : true;
}

createMenu(конфігураційного меню);

Добре:
const конфігураційного меню = {
title: 'Order',
// Користувач не має властивості 'body'
buttonText: 'Send',
cancellable: true
};

function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);

// config = {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}

createMenu(конфігураційного меню);

Не використовуйте прапори в якості параметрів функції

Прапори кажуть користувачеві, що функція здійснює більше одного дії. Функція повинна вирішувати одну задачу. Розділяйте функції, якщо вони виконують різні варіанти коду на основі логічного значення.
Погано:
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}

Добре:
function createFile(name) {
fs.create(name);
}

function createTempFile(name) {
createFile(`./temp/${name}`);
}

Уникайте побічних ефектів

Функція справляє побічний ефект, якщо вона робить будь-яку дію крім отримання значення і повернення іншого значення або значень. Побічний ефект може бути записом у файл, зміною якихось глобальних змінних або випадковим перекладом всіх ваших грошей невідомим особам.
Втім, побічні ефекти в програмі необхідні. Нехай, як і в попередньому прикладі, вам потрібно запис в файл. Опишіть те, що ви хочете зробити, суворо в одному місці.
Не створюйте кілька функцій і класів, які пишуть щось в конкретний файл. Створіть один сервіс, який всім цим займається. Один і тільки один.
Суть в тому, щоб уникати поширених помилок, таких як, наприклад, передача стану між об'єктами без якої-небудь структури, за допомогою змінних даних, які можуть перезаписувати хто завгодно, в обхід централізованого місця застосування побічних ефектів.
Якщо навчитеся так робити, ви станете щасливішими, ніж переважна більшість інших програмістів.
Погано:
// Глобальна змінна, на яку посилається наступна функція.
// Якщо б у нас була ще одна функція, яка б працювала з ім'ям name як з рядком,
// виявивши масив, він неодмінно б поламалася
let name = 'Ryan McDermott';

function splitIntoFirstAndLastName() {
name = name.split(' ');
}

splitIntoFirstAndLastName();

console.log(name); // ['Group', 'McDermott'];

Добре:
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}

const name = 'Ryan McDermott';
const newName = splitIntoFirstAndLastName(name);

console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Group', 'McDermott'];

Не переопределяйте глобальні функції

Забруднення глобальних змінних — погана практика в JavaScript, так як може породити конфлікти з іншою бібліотекою, і користувач вашого API не побачить помилок, поки не отримає виняток в продакшені. Давайте розглянемо приклад: що робити, якщо ви хочете розширити стандартний функціонал Array з JavaScript, додавши метод diff, який обчислював відмінність між двома масивами? Ви повинні були б записати нову функцію в Array.prototype, але тоді вона може увійти в конфлікт з іншою бібліотекою, яка намагалася зробити те ж саме. А якщо інша бібліотека використовувала метод diff, щоб знайти різницю між першим і останнім елементами масиву? Саме тому набагато краще використовувати класи ES2015/ES6 і просто успадкувати нашу реалізацію від класу Array.
Погано:
Array.prototype.diff = function diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
};

Добре:
class SuperArray extends Array {
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(elem => !hash.has(elem));
}
}

Віддавайте перевагу фунциональному програмування над иперативным

JavaScript не настільки функціональний мову, як Haskell, але певної частки функціональності він не позбавлений. Функціональні мови чистіше і простіше тестувати. Застосовуйте функціональний стиль програмування при можливості.
Погано:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];

let totalOutput = 0;

for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}

Добре:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];

const totalOutput = programmerOutput
.map((programmer) => programmer.linesOfCode)
.reduce((acc, linesOfCode) => acc + linesOfCode, 0);

Инкапсулируйте умови

Погано:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}

Добре:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}

Уникайте негативних умов

Погано:
function isDOMNodeNotPresent(node) {
// ...
}

if (!isDOMNodeNotPresent(node)) {
// ...
}

Добре:
function isDOMNodePresent(node) {
// ...
}

if (isDOMNodePresent(node)) {
// ...
}

Уникайте умовних конструкцій

Таке завдання здається неможливою. Почувши подібне, більшість людей кажуть: «Як я повинен робити що-небудь без вираження if?». Відповідь полягає в тому, що в багатьох випадках для досягнення тих же цілей можна використовувати поліморфізм. Друге питання, як правило, звучить так: «Добре, чудово, але чому я повинен їх уникати?». Відповідь — попередня концепція чистого коду, яку ми дізналися: функція повинна виконувати тільки одну задачу. Якщо у вас є класи та функції, що містять конструкцію 'if', ви ніби говорите своєму користувачеві, що ваша функція виконує більше однієї задачі. Пам'ятайте: одна функція — одна задача.
Погано:
class Airplane {
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}
}

Добре:
class Airplane {
// ...
}

class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount();
}
}

class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude();
}
}

class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure();
}
}

Уникайте перевірки типів (частина 1)

JavaScript є нетипизированным мовою, а це значить, що ваші функції можуть приймати аргументи будь-якого типу. Деколи ви обпікалися цією свободою, що спонукало вас проводити перевірку типів у ваших функціях. Є безліч способів її уникнути. В першу чергу варто подумати над узгодженим API.
Погано:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.peddle(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}

Добре:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}

Уникайте перевірки типів (частина 2)

Якщо ви працюєте з базовими примітивами, такими як рядки, цілі числа і масиви, і не можете використовувати поліморфізм, хоча все ще відчуваєте необхідність у перевірках типу, вам варто розглянути можливість застосування TypeScript. Це відмінна альтернатива звичайному JavaScript, надає можливість статичної типізації поверх стандартного синтаксису JavaScript. Проблема з ручною перевіркою типів у звичайному JavaScript в тому, що ілюзія безпеки, яку вона створює, ніяк не компенсується втратою читабельності з-за многословности коду. Тримайте код у чистоті, пишіть гарні тести і робіть ефективні ревізії коду. Або робіть все те ж саме, але з допомогою TypeScript (який, як я вже сказав, є прекрасною альтернативою!).
Погано:
function combine(val1, val2) {
if (typeof val1 === 'number' && typeof val2 === 'number' ||
typeof val1 === 'string' && typeof val2 === 'string') {
return val1 + val2;
}

throw new Error('Must be of type String or Number');
}

Добре:
function combine(val1, val2) {
return val1 + val2;
}

Не оптимізуйте надміру

Сучасні браузери виробляють безліч оптимізацій під капотом під час виконання коду. Оптимізуючи код вручну, ви, найчастіше, просто витрачаєте свій час. Є прекрасні ресурси з описом ситуацій, коли оптимізація дійсно кульгає. Поглядайте на них у вільний час, поки ці проблеми не будуть виправлені, якщо взагалі будуть, звичайно.
Погано:
// В старих браузерах кожна ітерація з некешованої `list.length` буде дорогою
// через перерахунку `list.length`. В сучасних браузерах це оптимізовано.
for (let i = 0, len = list.length; i < len; i++) {
// ...
}

Добре:
for (let i = 0; i < list.length; i++) {
// ...
}

Видаляйте мертвий код

Мертвий код так само поганий, як повторюваний код. Немає ніяких причин, щоб тримати його в репозиторії. Якщо код не викликається, позбудьтеся від нього!
Він як і раніше буде в системі контролю версій, якщо коли-небудь він все-таки вам знадобиться.
Погано:
function oldRequestModule(url) {
// ...
}

function newRequestModule(url) {
// ...
}

const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

Добре:
function newRequestModule(url) {
// ...
}

const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

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

0 коментарів

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