S.O.L.I.D Перші 5 принципів об'єктно-орієнтованого дизайну з JavaScript

Я знайшов дуже гарну статтю, яка пояснює S.O.L.I.D. принципи, якщо ви знайомі з PHP, ви можете прочитати тут оригінальну статтю: S.O.L.I.D: Перші 5 принципів об'єктно-орієнтованого дизайну. Але оскільки я розробник JavaScript, я адаптував приклади коду зі статті в JavaScript.

JavaScript - це мова з типовим типом, деякі вважають її функціональною мовою, інші вважають її об'єктно-орієнтованою мовою, деякі вважають її обома, а деякі вважають, що наявність класів у JavaScript просто невірна. - Дор Цур

Це просто проста «ласкаво просимо до статті S.O.L.I.D.», вона просто проливає світло на те, що S.O.L.I.D. є.

S.O.L.I.D. ВИСТУПАЄ ЗА:

  • S - Принцип єдиної відповідальності
  • O - відкритий закритий принцип
  • L - принцип заміщення Ліскова
  • I - принцип поділу інтерфейсу
  • D - принцип інверсії залежності

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

Клас повинен мати одну і єдину причину зміни, тобто, клас повинен мати лише одну роботу.

Наприклад, скажімо, у нас є деякі форми, і ми хотіли підбити підсумки всіх областей фігур. Ну це досить просто так?

const круг = (радіус) => {
  const proto = {
    тип: "Коло",
    // код
  }
  повернути Object.assign (Object.create (прото), {радіус})
}
const square = (довжина) => {
  const proto = {
    тип: "Квадрат",
    // код
  }
  повернути Object.assign (Object.create (прото), {length})
}

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

Що таке заводська функція?

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

Далі ми продовжуємо, створюючи заводську функцію areaCalculator, а потім записуємо свою логіку, щоб підбити підсумки всіх наданих фігур.

const areaCalculator = (s) => {
  const proto = {
    sum () {
      // логіка підсумовувати
    },
    вихід () {
     повернути `
       

         Сума площ наданих форм:          $ {this.sum ()}        

    }   }   повернути Object.assign (Object.create (прото), {shape: s}) }

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

const фігури = [
  коло (2),
  квадрат (5),
  квадрат (6)
]
const області = областьCalculator (фігури)
console.log (areas.output ())

Проблема методу виводу полягає в тому, що областьCalculator обробляє логіку для виведення даних. Отже, що робити, якщо користувач захотів вивести дані як json чи щось інше?

Всю логіку оброблятиме заводська функція areaCalculator, саме на це нахиляється «принцип єдиної відповідальності»; заводська функція areaCalculator повинна підсумовувати лише площі наданих фігур, не важливо, бажає користувач JSON або HTML.

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

Фабрична функція sumCalculatorOutputter працює так:

const фігури = [
  коло (2),
  квадрат (5),
  квадрат (6)
]
const області = областьCalculator (фігури)
const output = sumCalculatorOputter (областей)
console.log (вихід.JSON ())
console.log (вихід.HAML ())
console.log (output.HTML ())
console.log (вихід.JADE ())

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

# Принцип відкритого закриття

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

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

sum () {
 
 const область = []
 
 for (форма цієї форми) {
  
  if (shape.type === 'Квадрат') {
     area.push (Math.pow (shape.length, 2)
   } else if (shape.type === 'Коло') {
     area.push (Math.PI * Math.pow (shape.length, 2)
   }
 }
 повернути область.reduce ((v, c) => c + = v, 0)
}

Якби ми хотіли, щоб метод sum міг підсумовувати області більшої кількості фігур, нам доведеться додати більше if / else блоків, і це суперечить принципу відкритого закриття.

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

const square = (довжина) => {
  const proto = {
    тип: "Квадрат",
    область () {
      повернути Math.pow (this.length, 2)
    }
  }
  повернути Object.assign (Object.create (прото), {length})
}

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

sum () {
 const область = []
 for (форма цієї форми) {
   area.push (shape.area ())
 }
 повернути область.reduce ((v, c) => c + = v, 0)
}

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

Кодування до інтерфейсу є невід'ємною частиною S.O.L.I.D., швидкий приклад - ми створюємо інтерфейс, який реалізує кожна форма.

Оскільки у JavaScript немає інтерфейсів, я вам покажу, як це буде досягнуто в TypeScript, оскільки TypeScript моделює класичний OOP для JavaScript, і чим відрізняється чистий JavaScript Prototypal OO.

інтерфейс ShapeInterface {
 область (): число
}
клас Circle реалізує ShapeInterface {
 нехай радіус: число = 0
 конструктор (r: число) {
  this.radius = r
 }
 
 загальна площа (): номер {
  повернути MATH.PI * MATH.pow (this.radius, 2)
 }
}

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

Тож як ми могли цього досягти за браком інтерфейсів?

Функція Склад на допомогу!

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

const shapeInterface = (стан) => ({
  тип: 'shapeInterface',
  область: () => state.area (стан)
})

Тоді ми реалізуємо це до нашої квадратної фабричної функції.

const square = (довжина) => {
  const proto = {
    довжина,
    тип: "Квадрат",
    область: (args) => Math.pow (args.length, 2)
  }
  основи const = shapeInterface (прото)
  const composite = Object.assign ({}, основи)
  повернути Object.assign (Object.create (композитний), {length})
}

І результат виклику квадратної фабричної функції буде наступним:

const s = квадрат (5)
console.log ('OBJ \ n', s)
console.log ("PROTO \ n", Object.getPrototypeOf (s))
s.area ()
// вихід
OBJ
 {довжина: 5}
ПРОТО
 {type: 'shapeInterface', область: [Функція: область]}
25

У нашому методі підрахунку суми areaCalculator ми можемо перевірити, чи дійсно фігури, що надаються, є типами shapeInterface, інакше ми викинемо виняток:

sum () {
  const область = []
  for (форма цієї форми) {
    якщо (Object.getPrototypeOf (shape) .type === 'shapeInterface') {
       area.push (shape.area ())
     } else {
       кинути нову помилку ("це не об'єкт shapeInterface")
     }
   }
   повернути область.reduce ((v, c) => c + = v, 0)
}

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

# Принцип заміщення Ліскова

Нехай q (x) - властивість, доказувальна щодо об'єктів x типу T. Тоді q (y) має бути доказовим для об'єктів y типу S, де S є підтипом T.

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

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

Ще використовуючи нашу заводську функцію areaCalculator, скажімо, у нас є заводська функція volumeCalculator, яка розширює заводську функцію areaCalculator, а в нашому випадку для розширення об'єкта без порушення змін у ES6 ми робимо це за допомогою Object.assign () та Object. getPrototypeOf ():

const volumeCalculator = (s) => {
  const proto = {
    тип: 'volumeCalculator'
  }
  const areaCalProto = Object.getPrototypeOf (areaCalculator ())
  const nasledit = Object.assign ({}, областьCalProto, прото)
  повернути Object.assign (Object.create (успадкувати), {shape: s})
}

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

Клієнта ніколи не слід змушувати впроваджувати інтерфейс, який він не використовує, або клієнтів не слід змушувати залежати від методів, які вони не використовують.

Продовжуючи наш приклад фігур, ми знаємо, що у нас також є суцільні форми, тому оскільки ми також хотіли б обчислити об'єм фігури, ми можемо додати ще один контракт до shapeInterface:

const shapeInterface = (стан) => ({
  тип: 'shapeInterface',
  область: () => state.area (штат),
  том: () => state.volume (стан)
})

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

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

const shapeInterface = (стан) => ({
  тип: 'shapeInterface',
  область: () => state.area (стан)
})
const solidShapeInterface = (стан) => ({
  тип: 'solidShapeInterface',
  том: () => state.volume (стан)
})
const cubo = (довжина) => {
  const proto = {
    довжина,
    тип: "Cubo",
    область: (args) => Math.pow (args.length, 2),
    об'єм: (args) => Math.pow (args.length, 3)
  }
  основи const = shapeInterface (прото)
  const complex = solidShapeInterface (прото)
  const composite = Object.assign ({}, основи, комплекс)
  повернути Object.assign (Object.create (композитний), {length})
}

Це набагато кращий підхід, але дефіцит, на який слід слідкувати, - це коли обчислити суму для фігури, а не використовувати shapeInterface або solidShapeInterface.

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

const ManagShapeInterface = (fn) => ({
  тип: 'managementShapeInterface',
  обчислити: () => fn ()
})
const круг = (радіус) => {
  const proto = {
    радіус,
    тип: "Коло",
    область: (args) => Math.PI * Math.pow (args.radius, 2)
  }
  основи const = shapeInterface (прото)
  const abstraccion = ManagShapeInterface (() => basics.area ())
  const composite = Object.assign ({}, основи, абстракція)
  повернути Object.assign (Object.create (композитний), {радіус})
}
const cubo = (довжина) => {
  const proto = {
    довжина,
    тип: "Cubo",
    область: (args) => Math.pow (args.length, 2),
    об'єм: (args) => Math.pow (args.length, 3)
  }
  основи const = shapeInterface (прото)
  const complex = solidShapeInterface (прото)
  const abstraccion = ManagShapeInterface (
    () => basics.area () + complex.volume ()
  )
  const composite = Object.assign ({}, основи, абстракція)
  повернути Object.assign (Object.create (композитний), {length})
}

Як ви бачите дотепер, те, що ми робимо для інтерфейсів у JavaScript, - це фабричні функції для складання функцій.

І ось тут, з управлінняShapeInterface, ми знову абстрагуємо функцію обчислення, те, що ми робимо тут, і в інших інтерфейсах (якщо це можна назвати інтерфейсами), ми використовуємо «функції високого порядку» для досягнення абстракцій.

Якщо ви не знаєте, що таке функція вищого порядку, можете перейти до цього відео.

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

Суб'єкти повинні залежати від абстракцій, а не від конкрементів. У ньому зазначається, що модуль високого рівня не повинен залежати від модуля низького рівня, але вони повинні залежати від абстракцій.

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

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

Як інверсія залежності пов'язана з функціями вищого порядку? - питання, яке задають у stackExchange, якщо ви хочете глибокого пояснення.

Це може здатися роздутим, але це зрозуміти дуже просто. Цей принцип дозволяє проводити роз'єднання.

І ми це робили раніше, давайте переглянемо наш код за допомогою управлінняShapeInterface та як ми можемо виконати метод обчислення.

const ManagShapeInterface = (fn) => ({
  тип: 'managementShapeInterface',
  обчислити: () => fn ()
})

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

const square = (радіус) => {
  // код
 
  const abstraccion = ManagShapeInterface (() => basics.area ())
 
 // більше коду ...
}
const cubo = (довжина) => {
  // код
  const abstraccion = ManagShapeInterface (
    () => basics.area () + complex.volume ()
  )
  // більше коду ...
}

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

# Повні приклади коду

  • Ви можете отримати його тут: solid.js

# Подальше читання та посилання

  • РОЗКРИТИ перші 5 принципів OOD
  • 5 Принципів, які зроблять вас СОЛІДНИЙ JavaScript Developer
  • СЕРІДНА Серія JavaScript
  • Принципи твердості з використанням Typescript

# Висновок

"Якщо ви дотримуєтесь принципів SOLID до їхніх крайнощів, ви доходите до того, що робить функціональне програмування досить привабливим" - Марк Семан

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

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

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

Ви можете піти за мною #twitter @ cramirez_92
https://twitter.com/cramirez_92

До наступного разу