SOLID — это аббревиатура, представляющая набор из пяти принципов проектирования, которые помогают сделать разработку программного обеспечения более понятной, гибкой и удобной в сопровождении. Представленные Робертом С. Мартином, эти принципы стали необходимыми для создания масштабируемого и надежного программного обеспечения.

  • SRP: принцип единой ответственности
  • OCP: принцип открытия/закрытия
  • LSP: принцип замещения Лискова
  • ISP: принцип разделения интерфейсов
  • DIP: принцип внедрения зависимостей

В этом сообщении блога представлен обзор принципов SOLID и показано их применение на примерах TypeScript.

Принцип единой ответственности (SRP)

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

Пример машинописного текста:

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

class UserRepository {
  getUser(id: number): User {
    // code to fetch user from a data source
  }

  saveUser(user: User): void {
    // code to save user to a data source
  }
}

class UserValidator {
  isValid(user: User): boolean {
    // code to validate user data
  }
}

Здесь класс UserRepository отвечает за получение и сохранение пользователей, а класс UserValidator отвечает за проверку.

Открытый/закрытый принцип (OCP)

Принцип открытости/закрытости гласит, что программные объекты должны быть открыты для расширения, но закрыты для модификации. Это означает, чтокласс должен легко расширяться без изменения существующего кода.

Пример машинописного текста:

Рассмотрим систему обработки платежей, которая поддерживает несколько способов оплаты:

class PaymentProcessor {
  process(payment: Payment) {
    if (payment.type === "creditCard") {
      // code to process credit card payment
    } else if (payment.type === "paypal") {
      // code to process PayPal payment
    }
  }
}

Этот дизайн нарушает принцип открытости/закрытости, потому что для добавления нового способа оплаты необходимо изменить класс PaymentProcessor. Лучшим подходом будет:

interface Payment {
  process(): void;
}

class PaymentProcessor {
  process(payment: Payment): void {
    payment.process();
  }
}

class CreditCardPayment implements Payment {
  process(): void {
    // code to process credit card payment
  }
}

class PayPalPayment implements Payment {
  process(): void {
    // code to process PayPal payment
  }
}

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

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

Принцип подстановки Лисков гласит, что объекты суперкласса должны иметь возможность заменяться объектами подкласса без ущерба для правильности программы.

Пример машинописного текста:

Рассмотрим простой пример с прямоугольниками и квадратами:

class Rectangle {
  constructor(private width: number, private height: number) {}

  setWidth(width: number): void {
    this.width = width;
  }

  setHeight(height: number): void {
    this.height = height;
  }

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

class Square extends Rectangle {
  constructor(sideLength: number) {
    super(sideLength, sideLength);
  }

  setWidth(width: number): void {
    this.width = this.height = width;
  }

  setHeight(height: number): void {
    this.width = this.height = height;
  }
}

Хотя может показаться разумным наследовать Square от Rectangle, этот дизайн нарушает LSP, потому что Square не является подходящей заменой Rectangle (ширина и высота квадрата всегда равны). Лучшим решением было бы иметь отдельные классы для Rectangle и Square или использовать более общий класс Polygon.

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

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

Пример машинописного текста:

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

interface Device {
  turnOn(): void;
  turnOff(): void;
}

interface WiFiConnectable {
  connectToWiFi(): void;
}

interface MusicPlayable {
  playMusic(): void;
}

class SmartSpeaker implements Device, WiFiConnectable, MusicPlayable {
  turnOn(): void {
    // code to turn on the speaker
  }

  turnOff(): void {
    // code to turn off the speaker
  }

  connectToWiFi(): void {
    // code to connect to Wi-Fi
  }

  playMusic(): void {
    // code to play music
  }
}

class SmartLightBulb implements Device, WiFiConnectable {
  turnOn(): void {
    // code to turn on the light bulb
  }

  turnOff(): void {
    // code to turn off the light bulb
  }

  connectToWiFi(): void {
    // code to connect to Wi-Fi
  }
}

В этом примере мы создали отдельные интерфейсы для каждой функции (Device, WiFiConnectable, MusicPlayable). Класс SmartSpeaker реализует все три интерфейса, а класс SmartLightBulb реализует только интерфейсы Device и WiFiConnectable. Таким образом, ни один класс не будет вынужден реализовывать методы, которые ему не требуются.

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

Принцип инверсии зависимостей гласит, что модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций. Кроме того, абстракции не должны зависеть от деталей; детали должны зависеть от абстракций.

Пример машинописного текста:

Рассмотрим простую систему уведомлений по электронной почте:

interface NotificationService {
  sendNotification(email: string, content: string): void;
}

class EmailService implements NotificationService {
  sendNotification(email: string, content: string): void {
    // code to send an email
  }
}

class SMSNotificationService implements NotificationService {
  sendNotification(phoneNumber: string, content: string): void {
    // code to send an SMS
  }
}

class NotificationSystem {
  constructor(private notificationService: NotificationService) {}

  sendNotification(contact: string, content: string): void {
    this.notificationService.sendNotification(contact, content);
  }
}

Теперь NotificationSystem зависит от абстракции (NotificationService), что упрощает замену службы электронной почты другой службой уведомлений (например, SMS или push-уведомлениями) без изменения класса NotificationSystem.

Заключение

Принципы SOLID обеспечивают прочную основу для разработки удобных в сопровождении и масштабируемых программных систем. Придерживаясь этих принципов, разработчики могут создавать код, который легче понять, изменить и расширить. В этом посте мы рассмотрели каждый принцип SOLID и предоставили примеры TypeScript для демонстрации их применения. Эти принципы остаются актуальными и ценными при разработке приложений TypeScript и могут помочь улучшить общее качество кодовой базы.