Принципы SOLID в JavaScript с примерами

RedDeveloper
11.03.2023 13:20
Принципы SOLID в JavaScript с примерами

SOLID означает 5 из следующих  принципов

Принцип единой ответственности подразумевает то, что:

  • Для изменения модуля должна быть одна единственная причина

При нарушении принципа единой ответственности:

class ConcertLineups {
  constructor(maximumBandLimits) {
    this.lineups = [];
  }
  addBandsToLineup(bandName) {
    this.lineups.push(bandName);
  }
  displayLineups() {
    console.info(this.lineups);
  }
}
const concertLineups = new ConcertLineups();
concertLineups.addBandsToLineup('Warfaze');
concertLineups.addBandsToLineup('Karnival');
concertLineups.addBandsToLineup('SBC');
concertLineups.displayLineups();

Здесь у модуля ConcertLineups есть две причины для изменения:

  • Как мы добавляем составы
  • Как мы отображаем составы

Сейчас мы просто консолидируем составы. Позже может возникнуть потребность сохранить журнал или отправить его в аналитическую службу.

Более эффективным является подход, использующий принципы единой ответственности:

class Logger {
  log(message) {
      console.info(message);
  }
}

class ConcertLineups {
  constructor(maximumBandLimits) {
    this.lineups = [];
    this.logger = new Logger();
  }
  addBandsToLineup(bandName) {
    this.lineups.push(bandName);
  }
  displayLineups() {
    this.logger.log(this.lineups);
  }
}
const concertLineups = new ConcertLineups();
concertLineups.addBandsToLineup('Warfaze');
concertLineups.addBandsToLineup('Karnival');
concertLineups.addBandsToLineup('SBC');
concertLineups.displayLineups();

Здесь мы изолируем логирование в отдельном модуле. В любой момент, когда механизм протоколирования будет изменен, мы можем просто обновить класс Logger.

Открытый закрытый принцип

  • Модуль должен быть открыт для расширения, но закрыт для модификации

Рассмотрим следующий код, если мы обновляем другой платеж, нам нужно добавить еще один случай переключения. Это модифицирует существующий класс PaymentProcessor.Open closed pr

class StripePayment {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }
}

class PaypalPayment {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }
}
class PaymentProcessor {
  constructor(paymentAdaptar) {
    this.paymentAdaptar = paymentAdaptar;
  }
  pay() {
    switch(this.paymentAdaptar.paymentType) {
      case 'Stripe':
        makeStripePayment();
        break;
      case 'Paypal':
        makePaypalPayment();
        break;
    }
  }
}
const makeStripePayment = () => {
  // make payment
}
const makePaypalPayment = () => {
  // make payment
}

Вместо этого мы можем использовать следующий способ, при котором в любой момент, когда нам понадобится новый платежный адаптер, мы можем передать его в PaymentProcessor, без необходимости обновлять класс.

class StripePayment {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }
  
  makePayment () {
    // pay with stripe
  }
}

class PaypalPayment {
  constructor(paymentType) {
    this.paymentType = paymentType;
  }
  makePayment() {
    // pay with paypal
  }
}

class PaymentProcessor {
  constructor(paymentAdaptar) {
    this.paymentAdaptar = paymentAdaptar;
  }
  pay() {
    this.paymentAdaptar.makePayment();
  }
}

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

Принцип замещения Лискова подразумевает, что:

  • Объекты суперкласса должны вести себя как объекты подкласса, они должны быть заменяемыми

У нас есть суперкласс, Vehicle. Мы создали два подкласса Car и Cycle из Vehicle. У автомобиля есть метод startEngine. Но, похоже, что у Cycle нет двигателя.

Поэтому объект Vehicle и объект Cycle ведут себя не одинаково. Рассмотрим следующий пример:

class Vehicle {
  constructor(name) {
    this.name = name;
  }
  startEngine() {
    console.info(`${this.name} engine started`);
  }
}
class Car extends Vehicle {
  constructor(name) {
    super(name);
  }
}
class Cycle extends Vehicle {
  constructor(name) {
    super(name);
  }
  startEngine() {
    throw new Error(`${this.name} does not have an engine.`)
  }
}
const car = new Car('My Car');
car.startEngine();
const cycle = new Cycle('My Cycle');
cycle.startEngine();

Чтобы решить эту проблему, мы можем создать еще два подкласса после Vehicle, один MotorVehicle, который имеет двигатель, а другой ManualVehicle, который не имеет двигателя.

class Vehicle {
  constructor(name) {
    this.name = name;
  }
}
class MotorVehicle {
  constructor(name) {
    this.name = name;
  }
  startEngine() {
    console.info(`${this.name} engine started`);
  }
}
class ManualVehicle {
  constructor(name) {
    this.name = name;
  }
  startMoving() {
    console.info(`${this.name} started moving`);
  }
}
class Car extends MotorVehicle {
  constructor(name) {
    super(name);
  }
}
class Cycle extends ManualVehicle {
  constructor(name) {
    super(name);
  }
  startEngine() {
    throw new Error(`${this.name} does not have an engine.`)
  }
}
const car = new Car('My Car');
car.startEngine();
const cycle = new Cycle('My Cycle');
cycle.startMoving();

Принцип разделения интерфейсов

Принцип разделения интерфейсов подразумевает, что интерфейс не должен иметь никаких свойств, которые не используются или не требуются классу.

В следующем примере у нас есть интерфейс Shape, который реализован в классе Square и классе Cube. Мы можем вычислить объем для куба, но не для квадрата. Поэтому, когда мы реализуем Shape для класса Square, он выдает ошибку для метода volume.

interface Shape {
  area: () => void;
  volume: () => void;
}

class Square implements Shape {
  height: number;
  width: number;
  constructor(height: number) {
    this.height = height;
    this.width = height;
  }
  area () {
    console.info(this.height * this.width);
  }
  volume() {
    throw new Error('Volume can not valculated on 2d shape');
  }
}
class Cube implements Shape {
  height: number;
  width: number;
  length: number;
  constructor(height: number, width: number, length: number) {
    this.height = height;
    this.width = height;
    this.length = length;
  }
  area () {
    console.info(this.height * this.width);
  } 
  volume () {
    console.info(this.height * this.width * this.length);
  } 
}
const square = new Square(5);
square.area();
square.volume();
const cube = new Cube(5, 6, 7);
cube.area();
cube.volume();

L использовать при создании класса Cube.

interface Shape {
  area: () => void;
}

interface Shape3D extends Shape {
  volume: () => void;
}
class Square implements Shape {
  height: number;
  width: number;
  constructor(height: number) {
    this.height = height;
    this.width = height;
  }
  area () {
    console.info(this.height * this.width);
  }
}
class Cube implements Shape3D {
  height: number;
  width: number;
  length: number;
  constructor(height: number, width: number, length: number) {
    this.height = height;
    this.width = height;
    this.length = length;
  }
  area () {
    console.info(this.height * this.width);
  } 
  volume () {
    console.info(this.height * this.width * this.length);
  } 
}
const square = new Square(5);
square.area();
const cube = new Cube(5, 6, 7);
cube.area();
cube.volume();

Принцип инверсии зависимостей

Принцип инверсии зависимостей подразумевает,

  • Модули высокого уровня не должны зависеть от модулей низкого уровня, оба должны зависеть от абстракцийДля решения этой проблемы мы можем ввести расширенный интерфейс Shape3D. Этот новый интерфейс будет иметь метод объема и Wil
  • Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций

В следующем коде мы создаем класс CourseService в соответствии с CourseController. Здесь Courservice не должен зависеть от CourseController, вместо этого оба должны зависеть от интерфейса.

class CourseService {
  get() {
    console.info('All the courses');
  }
}

class CourseController {
  constructor(courseService) {
    this.courseService = courseService;
  }
  getAllCourse() {
    this.courseService.get();
  }
}
const courseService = new CourseService();
const courseController = new CourseController(courseService);
courseController.getAllCourse();

Чтобы решить эту проблему, нужен сервис, от которого может зависеть CourseService,

interface Service {
  get: () => void;
}

class CourseService implements Service {
  get() {
    console.info('All the courses');
  }
}
class CourseController {
  courseService: Service;
  constructor(courseService: Service) {
    this.courseService = courseService;
  }
  getAllCourse() {
    this.courseService.get();
  }
}
const courseService = new CourseService();
const courseController = new CourseController(courseService);
courseController.getAllCourse();

Lean Code: SOLID - Бо учит JavaScript

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для чтения благодаря своей простоте. Кроме того, мы всегда хотим проверить самые последние возможности в наших проектах!

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий их языку и культуре.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип предназначен для представления неделимого значения.