Чистий javascript.Класи

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

Зміст:

Принцип єдиної відповідальності (SRP)

Як написано у clean code, «Повинна бути лише одна причина для зміни класу» (There should never be more than one reason for a class to change). Заманливо все засунути в один клас, як в дорожній чемодан. Проблема в тому, що ваш клас не буде концптуально пов'язаний, і ви будете часто змінювати його на кожен чих. Дуже важливо мінімізувати зміни у класі. Коли ви вносите зміни в клас з величезним функціоналом, важко відстежити наслідки ваших змін.

Погано:

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()) {
// ...
}
}
}

Принцип відкритості/закритості (OCP)

Як заявив Бертран Мейер, «сутності (класи, модулі, функції тощо) повинні бути відкриті для розширення, але закриті для модифікації» (software entities (classes modules, functions, etc.) should be open for extension, but closed for modification). Що це означає? Це означає, що ви повинні давати можливість розширити функціональність суті не змінюючи існуючий код.

Погано:

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 === 'httpNodeAdapter') {
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) => {
// обробляємо відповідь
});
}
}

Принцип підстановки Барбари Лисков

Це страшний термін для дуже простої концепції.

Визначення:

«Нехай q(x) є властивістю вірним щодо об'єктів x деякого типу T. Тоді q(y) також повинно бути вірним для об'єктів типу y S, де S є підтипом типу T.» Wikipedia Визначення ще гірше, ніж назва.

Суть полягає в тому, що якщо у вас є батьківський і дочірній класи, то вони можуть взаимозаменятся без помилок. Це може збивати з пантелику, так що давайте подивимося на класичний приклад площі прямокутника. Математично квадрат-це прямокутник, але якщо ви вирішите це завдання з помощию спадкування, то у вас будуть проблеми. Більш детально про принцип можна почитати тут.

Погано:

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(); // BAD: Will return 25 for Square. Should be 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() {
super();
this.width = 0;
this.height = 0;
}

setWidth(width) {
this.width = width;
}

setHeight(height) {
this.height = height;
}

getArea() {
return this.width * this.height;
}
}

class Square extends Shape {
constructor() {
super();
this.length = 0;
}

setLength(length) {
this.length = length;
}

getArea() {
return this.length * this.length;
}
}

function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
switch (shape.constructor.name) {
case 'Square':
shape.setLength(5);
break;
case 'Rectangle':
shape.setWidth(4);
shape.setHeight(5);
}

const area = shape.getArea();
shape.render(area);
});
}

const shapes = [new Rectangle(), new Rectangle(), new Square()];
renderLargeShapes(shapes);

Принцип поділу інтерфейсу (ISP)

В javascript відсутні интенфейсы, так що цей принцип не вийде використовувати повною мірою. Тим не менш важливо його використовувати, навіть при відсутності системи типів javascript.

ISP стверджує, що Користувачі не повинні залежати від класів, які вони не використовують» (Clients should not be forced to depend upon interfaces that they do not use). Інтерфейси це умовні угоди в JavaScript через неявній типізації. Гарним прикладом у 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() {} // Частіше вам не потрібна анімація при русі.
// ...
});

Добре:

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() {}
}
});

Принцип інверсії залежності (DIP)

Цей принцип свідчить дві важливі речі:

1. Модулі вищого рівня не повинні залежати від модулів нижчого рівня. Обидва повинні залежати від абстракцій.
2. В абстракціях не повинно бути деталей. Деталі повинні бути в дочірніх класах.

Спочатку важко зрозуміти цей принцип. Але якщо ви працювали з Angular.js ви бачили реалізацію цього принципу у вигляді Dependency Injection (DI). Незважаючи на те, що вони не є ідентичними поняттями, DIP дає можливість відмежувати модулі високого рівня від деталей модулів низького рівня і встановлення їх. Він може зробити це через DI. Цей принцип зменшує зв'язок між модулями. Якщо ваші модулі тісно пов'язані, їх важко рефакторіть.

Абстракції і є неявними угодами, які представляють інтерфейси в JavaScript. Тобто методи і властивості, що об'єкт/клас надає іншому об'єкту/класу. У наведеному нижче прикладі кожен екземпляр класу InventoryTracker буде мати метод requestItems.

Погано:

class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}

requestItem(item) {
// ...
}
}

class InventoryTracker {
constructor(items) {
this.items = items;

// Погано те, що ми створили залежність від конкретної реалізації запиту.
// тепер наш метод requestItems не абстрактний і залежить від цієї реалізації
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();

Віддавайте перевагу класів (ES2015 / ES6) над простими функціями (ES5)

C допомогою класичних (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() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}

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();
car.setColor('pink');
car.setMake('Ford');
car.setModel('F-150');
car.save();

Добре:

class Car {
constructor() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}

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()
.setColor('pink')
.setMake('Ford')
.setModel('F-150')
.save();

Віддавайте предочтение композиції над спадкуванням

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

Коли ж використовувати спадкування? Це залежить від конкретної проблеми. Ось список випадків, коли спадкування має більше сенсу, ніж композиція:

  1. Коли спадкування являє собою залежність «є», а не «має» (Human->Animal vs. User->UserDetails)
  2. Ви можете повторно використовувати клас (Люди можуть рухатися як і всі тварини).
  3. Ви хочете, зробивши зміни батьківського класу, змінити дочірні класи
    (Зміна витрати калорій всіх тварин, коли вони рухаються).
Погано:

class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}

// ...
}

// У співробітників є податкові дані. Податкові дані не можуть бути співробітником.
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);
}
// ...
}

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

0 коментарів

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