Original Repository: ryanmcdermott/clean-code-javascript
- Вступ
- Змінні
- Функції
- Об'єкти та структури даних
- Класи
- SOLID
- Тестування
- Асинхронність
- Обробка помилок
- Форматування
- Коментарі
- Переклад
Принципи програмної інженерії з книги Роберта С. Мартіна Clean Code, адаптовані для JavaScript. Це не гайд зі стилю кода. Це посібник для розробки програмного забезпечення на JavaScript, котре легко читати, повторно використовувати і рефакторити.
Не кожен згаданий тут принцип обов'язковий до дотримання, і лише деякі з них не викличуть розбіжностей. Це поради і нічого більше, але їх документували на основі багаторічного колективного досвіду авторів Clean Code.
Нашому ремеслу програмної інженерії трохи більше 50 років, і ми все ще багато чого пізнаємо. Коли архітектура програмного забезпечення буде такою ж старою, як і сама архітектура, можливо тоді в нас будуть більш жорсткі правила до дотримання. А зараз нехай ці поради слугують критерієм для оцінки JavaScript коду, який створюєте ви і ваша команда.
Ще одна річ: знання цих принципів не зробить вас кращим розробником миттєво, і багаторічна праця з ними не значить, що ви не будете робити помилок. Кожен фрагмент коду починається з чорнового варіанту, подібно мокрій глині, що набуває своєї кінцевої форми. По завершенню, ми винищуємо недоліки, коли рецензуємо код з колегами. Не коріть себе за перші чорнові версії коду, що потребують поліпшення. Поліпшуйте код замість цього!
Погано:
const yyyymmdstr = moment().format("YYYY/MM/DD");
Добре:
const currentDate = moment().format("YYYY/MM/DD");
Погано:
getUserInfo();
getClientData();
getCustomerRecord();
Добре:
getUser();
Ми прочитаємо більше коду, ніж коли-небудь напишемо. Важливо, щоб наш код був легким для читання і пошуку. Не використовуючи імена для змінних, що є важливими для розуміння нашої програми, ми шкодимо тим, хто читає наш код. Робіть імена доступними для пошуку. Такі засоби, як buddy.js та ESLint можуть допомогти у знаходженні безіменних констант.
Погано:
// Якого біса означає 86400000?
setTimeout(blastOff, 86400000);
Добре:
// Оголошуйте їх у якості іменованих констант з верхнім регістром.
const MILLISECONDS_IN_A_DAY = 86_400_000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
Погано:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);
Добре:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
Явне краще за неявне.
Погано:
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(l => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Почекайте, що там означає `l`?
dispatch(l);
});
Добре:
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
Якщо ім'я вашого класу/об'єкта щось говорить вам, не повторюйте це у назві змінної.
Погано:
const Car = {
carMake: "Honda",
carModel: "Accord",
carColor: "Blue"
};
function paintCar(car) {
car.carColor = "Red";
}
Добре:
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};
function paintCar(car) {
car.color = "Red";
}
Аргументи за замовчуванням часто є більш чистими, ніж коротке обчислення. Майте на увазі, що при
використанні аргументів за замовчуванням ваша функція надасть значення за замовчуванням тільки для undefined
аргументів. Інші "хибні" (falsy) значення, як-то ''
, ""
, false
, null
, 0
, і
NaN
, не будуть замінені значенням за замовчуванням.
Погано:
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
Добре:
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
Обмеження кількості параметрів функції надзвичайно важливо, тому що це робить тестування вашої функції простішим. Наявність більше трьох параметрів призводить до комбінаторного вибуху, коли вам потрібно тестувати безліч різноманітних ситуацій з кожним окремим аргументом.
Один або два аргументи є ідеальним випадком, трьох аргументів слід уникати по можливості. Якщо аргументів більше, їх слід об'єднати. Зазвичай, якщо у вас більше двох аргументів, ваша функція намагається виконувати забагато дій. В тих випадках, коли це не так, майже завжди буде достатньо об'єкта вищого рівня.
Так як JavaScript дозволяє вам створювати об'єкти на льоту, без використання синтаксису класів, ви можете використовувати об'єкт, якщо потребуєте багатьох аргументів.
Щоб було очевидно, які властивості функція очікує, ви можете використовувати ES2015/ES6 синтаксис деструктуризації. Такий підхід має декілька переваг:
- Коли хтось дивиться на сигнатуру функції, одразу зрозуміло, які властивості використовуються.
- Деструктуризацію можна використовувати, щоб імітувати іменовані параметри.
- Деструктуризація, окрім того, клонує вказані примітивні значення об'єкту аргумента, переданого у функцію. Це може допомогти запобіганню побічним ефектам. Примітка: об'єкти та масиви, які деструктурували з об'єкта аргументу, НЕ клонуються.
- Лінтери можуть попередити вас щодо невикористаних властивостей, що було б неможливим без деструктуризації.
Погано:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
Добре:
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
Це, безумовно, найважливіше правило в програмній інженерії. Коли функції виконують більше одної дії, їх важко поєднувати, тестувати та обґрунтовувати. Коли ви можете обмежити функцію до тільки одної дії, її можна легко рефакторити і ваш код буде набагато чистішим. Навіть якщо ви засвоїте з цього посібника тільки цю пораду, ви будете попереду багатьох розробників.
Погано:
function emailClients(clients) {
clients.forEach(client => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
Добре:
function emailActiveClients(clients) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(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 parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach(node => {
// парсинг...
});
}
function tokenize(code) {
const REGEXES = [
// ...
];
const statements = code.split(" ");
const tokens = [];
REGEXES.forEach(REGEX => {
statements.forEach(statement => {
tokens.push(/* ... */);
});
});
return tokens;
}
function parse(tokens) {
const syntaxTree = [];
tokens.forEach(token => {
syntaxTree.push(/* ... */);
});
return syntaxTree;
}
Робіть все можливе, щоб уникнути повторюваного коду. Повторюваний код поганий тому, що означає наявність більше одного місця для змін у разі, якщо вам потрібно буде змінити деяку логіку.
Уявіть, що ви керуєте рестораном і стежите за своїм інвентарем: помідори, цибуля, часник, спеції тощо. Якщо у вас є кілька списків, у яких ви все зберігаєте, тоді кожен з них потрібно оновлювати, коли ви подаєте страву з помідорами. Якщо у вас є лише один список, є лише одне місце для оновлення!
Часто у вас є повторюваний код, коли ви маєте дві або більше трохи різних сутностей, які мають багато спільного, але їх відмінності змушують вас мати дві або більше окремих функцій, які виконують багато однакових дій. Видалення повторюваного коду означає створення абстракції, яка може обробляти цю множину різних сутностей лише однією функцією/модулем/класом.
Правильне розуміння абстракції є критично важливим, саме тому ви повинні слідувати принципам 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 showEmployeeList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case "manager":
data.portfolio = employee.getMBAProjects();
break;
case "developer":
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
Погано:
const menuConfig = {
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(menuConfig);
Добре:
const menuConfig = {
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(menuConfig);
Флаги повідомляють користувачу, що ця функція виконує більше однієї дії. Функції повинні виконувати лише одну дію. Розділіть функції, якщо вони дотримуються різних кодових шляхів на основі булевої змінної.
Погано:
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}`);
}
Функція створює побічний ефект, якщо робить щось інше, окрім приймання вхідного значення і повернення іншого значення або значень. Побічним ефектом може бути запис у файл, зміна якоїсь глобальної змінної або випадкове пересилання всіх ваших грошей незнайомцю.
Проте, час від часу вам потрібно мати побічні ефекти в програмі. Як у попередньому прикладі, вам може знадобитися запис у файл. Що вам потрібно зробити - це централізувати місце, де ви виконуєте таку логіку. Не створюйте декількох функцій та класів, які роблять запис у певний файл. Майте один сервіс, який це робить. Один і тільки один.
Основна думка тут - це уникання поширених помилок, таких, як розділення стану між об'єктами без будь-якої структури, використання змінних типів даних, в які можна записати що завгодно, і відсутність централізування у місцях виникнення побічних ефектів. Якщо ви зможете це зробити, ви будете щасливішими за переважну більшість інших програмістів.
Погано:
// Глобальна змінна, на яку посилається наступна функція.
// Якщо б у нас була ще одна функція, що використовує це ім'я, то зараз ця змінна була б масивом
// і функція могла б зламати змінну.
let name = "Ryan McDermott";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
Добре:
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);
console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];
У JavaScript примітиви передаються за значенням, а об'єкти/масиви передаються за посиланням. У випадку об'єктів і масивів, якщо ваша функція вносить зміни у масив кошика для покупок, наприклад, додаючи товар для придбання, тоді це додавання вплине на будь-яку іншу функцію, яка використовує масив cart
. Це може бути чудово, проте це може бути і погано. Давайте уявимо погану ситуацію:
Користувач натискає кнопку "Придбати", кнопка викликає функцію purchase
, що створює мережевий запит і надсилає масив cart
на сервер. Через погане мережеве з'єднання, функція purchase
повинна створювати повторний запит. А якщо тим часом користувач випадково натисне "Додати в кошик" на товарі, який йому не потрібен, до початку мережевого запиту? Якщо це трапляється і мережевий запит починається, то функція придбання відправить випадково доданий товар, оскільки він має посилання на масив кошика покупок, котрий функція addItemToCart
модифікувала додаванням небажаного товару.
Чудовим рішенням для addItemToCart
було б завжди клонувати cart
, редагувати його і повертати клон. Це гарантує, що сторонні зміни не вплинуть на жодну функцію, що посилається на кошик для покупок.
Два застереження, які слід згадати при такому підході:
-
Можуть бути випадки, коли ви дійсно хочете змінити об'єкт на вході, але коли ви застосуєте цю практику програмування, ви виявите, що ці випадки є досить рідкісними. Більшість речей можна відрефакторити так, щоб уникнути побічних ефектів!
-
Клонування великих об'єктів може бути дуже дорогим з точки зору продуктивності. На щастя, це не є великою проблемою на практиці, оскільки є чудові бібліотеки, що дозволяють такому підходу програмування бути швидким і не таким вибагливим до пам'яті, як клонування об’єктів та масивів вручну.
Погано:
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
Добре:
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
Забруднення глобальних змінних є поганою практикою в JavaScript, оскільки у вас може виникнути конфлікт з іншою бібліотекою, і користувач вашого API не буде цього знати, доки не отримає виняток у продакшені. Давайте подумаємо про приклад: що, якби ви хотіли розширити нативний метод масиву JavaScript так, щоб Array
мав метод 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.reduce(
(totalLines, output) => totalLines + output.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();
}
}
JavaScript є слабо типізованою мовою - це означає, що ваші функції можуть приймати аргументи будь-якого типу. Іноді вам незручна ця свобода, і здається спокусливим зробити перевірку типів у ваших функціях. Є багато способів уникнути такої необхідності. Перше, що слід врахувати, - це послідовні API.
Погано:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(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"));
}
Якщо ви працюєте з базовими примітивними значеннями, такими як рядки та цілі числа, і ви не можете використовувати поліморфізм, але все ще відчуваєте потребу перевірити тип, вам слід розглянути можливість використання TypeScript. Це відмінна альтернатива звичайному JavaScript, оскільки вона надає вам статичну типізацію над стандартним 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");
Використання геттерів та сеттерів для доступу до даних об'єктів може бути кращим за просте отримання властивості об'єкта. "Чому?" - ви можете запитати. Ось перелік причин:
- Коли ви хочете зробити більше, ніж отримати властивість об’єкта, вам не потрібно шукати та змінювати кожне місце доступу до властивості об'єкта.
- Робить валідацію простою при роботі з
set
. - Інкапсулює внутрішнє представлення.
- Легко додавати логування та обробку помилок під час отримання та встановлення властивостей.
- Ви можете ліниво завантажити властивості об'єкта, скажімо, отримуючи їх з сервера.
Погано:
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
Добре:
function makeBankAccount() {
// ця властивість приватна
let balance = 0;
// "геттер", є публічним через повернутий об’єкт нижче
function getBalance() {
return balance;
}
// "сеттер", є публічним через повернутий об’єкт нижче
function setBalance(amount) {
// ... валідація перед оновленням балансу
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);
Цього можна досягти за допомогою замикань (для ES5 і нижче).
Погано:
const Employee = function(name) {
this.name = name;
};
Employee.prototype.getName = function getName() {
return this.name;
};
const employee = new Employee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined
Добре:
function makeEmployee(name) {
return {
getName() {
return name;
}
};
}
const employee = makeEmployee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
Дуже важко отримати читабельне наслідування класів, їх побудову та визначення методів у класичних ES5 класах. Якщо вам потрібне наслідування (майте на увазі, що це може бути не так), тоді віддайте перевагу ES2015 / ES6 класам. Однак віддавайте перевагу невеликим функціям над класами, доки вам не знадобляться більш великі і складні об'єкти.
Погано:
const Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error("Instantiate Animal with `new`");
}
this.age = age;
};
Animal.prototype.move = function move() {};
const Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error("Instantiate Mammal with `new`");
}
Animal.call(this, age);
this.furColor = furColor;
};
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};
const Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error("Instantiate Human with `new`");
}
Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};
Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};
Добре:
class Animal {
constructor(age) {
this.age = age;
}
move() {
/* ... */
}
}
class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}
liveBirth() {
/* ... */
}
}
class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}
speak() {
/* ... */
}
}
Цей паттерн дуже корисний у JavaScript, і ви спостерігаєте його у багатьох бібліотеках, таких як jQuery і Lodash. Прив'язка методів дозволяє вашому коду бути виразним і менш багатослівним. Спробуйте використати прив'язку методів і погляньте, наскільки чистим буде ваш код. У функціях класу просто поверніть this
в кінці кожної функції, і ви можете прив'язувати до нього подальші методи класу.
Погано:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
const car = new Car("Ford", "F-150", "red");
car.setColor("pink");
car.save();
Добре:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// ПРИМІТКА: Повертаємо this для прив'язування
return this;
}
setModel(model) {
this.model = model;
// ПРИМІТКА: Повертаємо this для прив'язування
return this;
}
setColor(color) {
this.color = color;
// ПРИМІТКА: Повертаємо this для прив'язування
return this;
}
save() {
console.log(this.make, this.model, this.color);
// ПРИМІТКА: Повертаємо this для прив'язування
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();
Як відомо зазначено в книзі Design Patterns Бандою Чотирьох, вам слід віддати перевагу композиції над наслідуванням, де це можливо. Є багато вагомих причин використовувати наслідування і є безліч вагомих причин використовувати композицію. Основним моментом цієї максими є те, що якщо ваш розум інстинктивно дотримується наслідування, спробуйте подумати, чи композиція могла б краще моделювати вашу проблему. У деяких випадках вона могла б.
Тоді вам може бути цікаво: "коли я повинен використовувати наслідування?" Це залежить від вашої поточної проблеми, але це пристойний перелік ситуацій, коли наслідування має більше сенсу, ніж композиція:
- Ваше наслідування представляє відносини типу "є чимось", а не "має щось" (Людина->Тварина проти Користувач->ДеталіКористувача).
- Ви можете повторно використовувати код з базових класів (Люди можуть рухатися, як і всі тварини).
- Ви хочете внести глобальні зміни до похідних класів, змінивши базовий клас. (Зміна витрати калорій всіх тварин, коли вони рухаються).
Погано:
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// Погано, тому що Співробітники "мають" податкові дані. EmployeeTaxData не є типом Employee
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}
Добре:
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}
Як зазначено в Чистому коді: "Ніколи не повинно бути більше однієї причини для зміни класу". Привабливо наповнити клас великою кількістю функціоналу, як у ситуації, коли ви можете взяти лише одну валізу у свій рейс. Проблема в тому, що ваш клас не буде концептуально єдиним, і це дасть йому багато причин для змін. Мінімізування кількості змін класу - це важливо. Це важливо тому, що якщо в одному класі забагато функціоналу і ви модифікуєте його частину, може бути важко зрозуміти, як це вплине на інші залежні модулі у вашій кодовій базі.
Погано:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Добре:
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
Як зазначає Бертран Меєр: "програмні об'єкти (класи, модулі, функції, тощо) мають бути відкритими для розширення, але закритими для внесення змін". Що мається на увазі? Загалом, цей принцип говорить, що ви повинні дозволити користувачам додавати новий функціонал без зміни існуючого коду.
Погано:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === "ajaxAdapter") {
return makeAjaxCall(url).then(response => {
// трансформувати відповідь і повернути її
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// трансформувати відповідь і повернути її
});
}
}
}
function makeAjaxCall(url) {
// виконати запит і повернути проміс
}
function makeHttpCall(url) {
// виконати запит і повернути проміс
}
Добре:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
request(url) {
// виконати запит і повернути проміс
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
request(url) {
// виконати запит і повернути проміс
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then(response => {
// трансформувати відповідь і повернути її
});
}
}
Це страшний термін для дуже простої концепції. Формально вона визначена так: "Якщо S є підтипом T, тоді об'єкти типу T можуть бути замінені об'єктами типу S (тобто об'єкти типу S можуть підставлятися замість об'єктів типу T), не змінюючи жодної важливої властивості програми (коректність, виконання завдань, тощо)". Це ще страшніше визначення.
Найкраще пояснення цьому - якщо у вас є батьківський клас та дочірній клас, то їх можна використовувати взаємозамінно, не отримуючи неправильних результатів. Це все ще може бентежити, тому давайте подивимось на класичний приклад Квадрат-Прямокутник. Математично квадрат є прямокутником, але якщо ви змоделюєте це за допомогою відносини "є чимось" шляхом наслідування, ви швидко отримаєте проблему.
Погано:
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach(rectangle => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // ПОГАНО: Повертає 25 для Квадрату. Повинно бути 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Добре:
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach(shape => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
У JavaScript немає інтерфейсів, тому цей принцип не застосовується так суворо, як інші. Однак він є важливим і актуальним навіть при відсутності системи типів у JavaScript.
ISP зазначає: "Клієнти не повинні залежати від інтерфейсів, які вони не використовують." Інтерфейси є неявними контрактами в JavaScript через качину типізацію (duck typing).
Хороший приклад, що демонструє цей принцип в JavaScript - це класи, які потребують великих об'єктів налаштувань. Не вимагати від клієнтів встановлення величезної кількості варіантів вигідно, тому що більшу частину часу вони не потребують всіх налаштувань. Якщо зробити налаштування необов’язковими, це допоможе запобігти появі "жирного інтерфейсу".
Погано:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Більшу частину часу ми не потребуємо анімації під час обходу DOM.
// ...
});
Добре:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
options: {
animationModule() {}
}
});
Цей принцип визначає дві важливі речі:
- Модулі високого рівня не повинні залежати від модулів низького рівня. І ті й інші повинні залежати від абстракцій.
- Абстракції не повинні залежати від деталей. Деталі повинні залежати від абстракцій.
Спочатку це може бути важко зрозуміти, але якщо ви працювали з AngularJS, ви бачили реалізацію цього принципу у формі впровадження залежностей (DI). Хоча вони не є ідентичними поняттями, DIP утримує модулі високого рівня від знання деталей модулів низького рівня та їх налаштування. Цього можна досягти за допомогою DI. Величезна перевага впровадження залежностей полягає в тому, що воно зменшує зв'язування між модулями. Зв'язування - це дуже поганий паттерн розробки, оскільки це робить ваш код важким для рефакторингу.
Як було сказано раніше, у JavaScript немає інтерфейсів, тому абстракції є неявними контрактами. Тобто методами та властивостями, які об'єкт/клас показує іншому об'єкту/класу. У наведеному нижче прикладі неявний контракт полягає в тому, що будь-який модуль запиту для InventoryTracker
матиме метод requestItems
.
Погано:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// ПОГАНО: Ми створили залежність від реалізації конкретного запиту.
// Треба щоб requestItems залежав тільки від методу запиту: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();
Добре:
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ["WS"];
}
requestItem(item) {
// ...
}
}
// Побудувавши залежності зовні та впровадивши їх, ми можемо легко
// замінити наш модуль запиту на новий та модний, що використовує вебсокети.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();
Тестування важливіше, ніж доставка коду. Якщо у вас немає тестів або їх кількість недостатня, то кожного разу, надсилаючи код, ви не будете впевнені, що нічого не зламали. Питання про те, що становить адекватну кількість тестів, вирішується вашою командою, але 100% покриття (усі вирази та гілки) - це те, як ви досягаєте дуже високої впевненості та спокою як розробник. Це означає, що окрім чудового фреймворку тестування, вам також потрібно використовувати гарний інструмент покриття.
Немає приводу не писати тестів. Існує безліч хороших JS фреймворків, тож знайдіть такий, якому віддає перевагу ваша команда. Коли ви знайдете такий, що підходить вашій команді, намагайтесь завжди писати тести для кожної нової функції/модуля, який ви вводите. Якщо вашим улюбленим методом є розробка через тестування (TDD), це чудово, але головне - просто переконайтеся, що ви досягаєте своїх планів з покриття тестами, перш ніж запускати будь-яку функцію або рефакторити існуючу.
Погано:
import assert from "assert";
describe("MomentJS", () => {
it("handles date boundaries", () => {
let date;
date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});
Добре:
import assert from "assert";
describe("MomentJS", () => {
it("handles 30-day months", () => {
const date = new MomentJS("1/1/2015");
date.addDays(30);
assert.equal("1/31/2015", date);
});
it("handles leap year", () => {
const date = new MomentJS("2/1/2016");
date.addDays(28);
assert.equal("02/29/2016", date);
});
it("handles non-leap year", () => {
const date = new MomentJS("2/1/2015");
date.addDays(28);
assert.equal("03/01/2015", date);
});
});
Колбеки не є чистими, і вони викликають надмірну кількість вкладеності. З ES2015 / ES6, проміси - це вбудований глобальний тип. Використовуйте їх!
Погано:
import { get } from "request";
import { writeFile } from "fs";
get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
(requestErr, response, body) => {
if (requestErr) {
console.error(requestErr);
} else {
writeFile("article.html", body, writeErr => {
if (writeErr) {
console.error(writeErr);
} else {
console.log("File written");
}
});
}
}
);
Добре:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
Проміси є дуже чистою альтернативою колбекам, але ES2017 / ES8 приносить async та await, які пропонують ще більш чисте рішення. Все, що вам потрібно, - це функція, яка має в префіксі ключове слово async
, і тоді ви можете імперативно писати логіку без прив'язки функцій до then
.
Погано:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
.then(body => {
return writeFile("article.html", body);
})
.then(() => {
console.log("File written");
})
.catch(err => {
console.error(err);
});
Добре:
import { get } from "request-promise";
import { writeFile } from "fs-extra";
async function getCleanCodeArticle() {
try {
const body = await get(
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
);
await writeFile("article.html", body);
console.log("File written");
} catch (err) {
console.error(err);
}
}
getCleanCodeArticle()
Викидання помилок - це гарна річ! Воно означає, що під час виконання программа успішно ідентифікувала, коли щось пішло не так, і дозволяє вам знати про це, зупиняючи виконання функції на поточному стеку, вбиваючи процесс (у Node) та повідомляючи вас трасировкою стеку у консолі.
Якщо нічого не робити із перехопленою помилкою, ви не зможете її виправити або зреагувати на неї. Логування помилки у консолі (console.log
) не набагато краще, оскільки часто цей лог може загубитися в морі того, що друкується у консолі. Якщо ви вкладаєте фрагмент коду в try/catch
, це означає, що ви
передбачаєте там помилку, і тому ви повинні мати план або створити шлях коду, коли помилка виникає.
Погано:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
Добре:
try {
functionThatMightThrow();
} catch (error) {
// Один варіант (більш помітний, ніж console.log):
console.error(error);
// Інший варіант:
notifyUserOfError(error);
// Інший варіант:
reportErrorToService(error);
// АБО використовуйте всі три!
}
З тих причин, що і перехоплені помилки з try/catch
.
Погано:
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});
Добре:
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
// Один варіант (більш помітний, ніж console.log):
console.error(error);
// Інший варіант:
notifyUserOfError(error);
// Інший варіант:
reportErrorToService(error);
// АБО використовуйте всі три!
});
Форматування суб'єктивне. Подібно до багатьох правил тут - не існує жорсткого припису, якого потрібно дотримуватися. Основний момент - НЕ СПЕРЕЧАЙТЕСЬ над форматуванням. Існує безліч інструментів для його автоматизації. Використовуйте один з них! Марно витрачати час і гроші, щоб інженери сперечалися з приводу форматування.
Для ситуацій, які не підпадають під автоматичне форматування (відступи, табуляції проти пробілів, подвійні проти одиничних лапок тощо), тут наявні декілька вказівок.
JavaScript не типізований, тому регістр багато говорить про ваші змінні, функції тощо. Ці правила є суб'єктивними, тому ваша команда може вибрати будь-які. Головне - незалежно від того, що ви обираєте, будьте послідовними.
Погано:
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const Artists = ["ACDC", "Led Zeppelin", "The Beatles"];
function eraseDatabase() {}
function restore_database() {}
class animal {}
class Alpaca {}
Добре:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const SONGS = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const ARTISTS = ["ACDC", "Led Zeppelin", "The Beatles"];
function eraseDatabase() {}
function restoreDatabase() {}
class Animal {}
class Alpaca {}
Якщо функція викликає іншу, тримайте ці функції вертикально близько у вихідному файлі. В ідеалі тримайте викликаючу функцію прямо над тою, котру викликають. Ми схильні читати код зверху вниз, як газету. Тому зробіть так, щоб ваш код читався таким чином.
Погано:
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
lookupPeers() {
return db.lookup(this.employee, "peers");
}
lookupManager() {
return db.lookup(this.employee, "manager");
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getManagerReview() {
const manager = this.lookupManager();
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();
Добре:
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}
perfReview() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
}
getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
lookupPeers() {
return db.lookup(this.employee, "peers");
}
getManagerReview() {
const manager = this.lookupManager();
}
lookupManager() {
return db.lookup(this.employee, "manager");
}
getSelfReview() {
// ...
}
}
const review = new PerformanceReview(employee);
review.perfReview();
Коментарі - це вибачення, а не вимога. Якісний код переважно документує себе сам.
Погано:
function hashIt(data) {
// Хеш
let hash = 0;
// Довжина рядка
const length = data.length;
// Проходимось циклом через кожний символ у даних
for (let i = 0; i < length; i++) {
// Отримуємо код символу.
const char = data.charCodeAt(i);
// Створюємо хеш
hash = (hash << 5) - hash + char;
// Конвертуємо в 32-бітне ціле число
hash &= hash;
}
}
Добре:
function hashIt(data) {
let hash = 0;
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data.charCodeAt(i);
hash = (hash << 5) - hash + char;
// Конвертуємо в 32-бітне ціле число
hash &= hash;
}
}
Контроль версій існує не просто так. Залиште старий код в історії.
Погано:
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
Добре:
doStuff();
Пам'ятайте - використовуйте контроль версій! Немає необхідності в мертвому коді, закоментованому коді,
і особливо в журнальних коментарях. Використовуйте git log
, щоб отримати історію!
Погано:
/**
* 2016-12-20: Видалив монади, не зрозумів їх (RM)
* 2016-10-01: Покращив використання спеціальних монад (JP)
* 2016-02-03: Видалив перевірку типів (LI)
* 2015-03-14: Додав combine з перевіркою типів (JR)
*/
function combine(a, b) {
return a + b;
}
Добре:
function combine(a, b) {
return a + b;
}
Зазвичай вони просто додають шум. Нехай функції та назви змінних разом із належними відступами та форматуванням надають візуальну структуру вашому коду.
Погано:
////////////////////////////////////////////////////////////////////////////////
// Ініціалізація властивості model об'єкта $scope
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: "foo",
nav: "bar"
};
////////////////////////////////////////////////////////////////////////////////
// Встановлення екшену
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
Добре:
$scope.model = {
menu: "foo",
nav: "bar"
};
const actions = function() {
// ...
};
Цей посібник також доступний на інших мовах:
- Вірменська: hanumanum/clean-code-javascript/
- Бенгальська: InsomniacSabbir/clean-code-javascript/
- Бразильська португальська: fesnt/clean-code-javascript
- Спрощена китайська:
- Традиційна китайська: AllJointTW/clean-code-javascript
- Французька: GavBaros/clean-code-javascript-fr
- Німецька: marcbruederlin/clean-code-javascript
- Індонезійська: andirkh/clean-code-javascript/
- Італійська: frappacchio/clean-code-javascript/
- Японська: mitsuruog/clean-code-javascript/
- Корейська: qkraudghgh/clean-code-javascript-ko
- Польська: greg-dev/clean-code-javascript-pl
- Російська:
- Іспанська: tureey/clean-code-javascript
- Іспанська: andersontr15/clean-code-javascript
- Турецька: bsonmez/clean-code-javascript
- В'єтнамська: hienvd/clean-code-javascript/