SOLID

SOLID

Картинка к публикации: SOLID

Принципы SOLID

Принципы SOLID представляют собой набор руководящих принципов и практик, разработанных для проектирования и разработки поддерживаемых, расширяемых и понятных программных систем. Эти принципы были сформулированы Робертом Мартином (также известным как "Дядя Боб") и стали одними из фундаментальных концепций в мире объектно-ориентированного программирования (ООП).

Акроним SOLID представляет собой следующие пять принципов:

  1.  Принцип единственной ответственности (Single Responsibility Principle, SRP): Каждый класс должен иметь только одну причину для изменения. Это означает, что класс должен быть ответственным только за одну конкретную часть функциональности.
  2.  Принцип открытости/закрытости (Open/Closed Principle, OCP): Программные сущности, такие как классы, модули и функции, должны быть открытыми для расширения, но закрытыми для модификации. Это позволяет добавлять новую функциональность, не изменяя существующий код.
  3.  Принцип подстановки Барбары Лисков (Liskov Substitution Principle, LSP): Объекты производных классов должны быть способны заменять объекты базовых классов без изменения ожидаемого поведения программы. Этот принцип подчеркивает важность согласованности интерфейсов и поведения наследников.
  4.  Принцип интерфейсов (Interface Segregation Principle, ISP): Клиенты не должны зависеть от интерфейсов, которые они не используют. Интерфейсы должны быть маленькими и специфическими для нужд клиентов.
  5.  Принцип инверсии зависимостей (Dependency Inversion Principle, DIP): Высокоуровневые модули не должны зависеть от низкоуровневых модулей. Оба типа модулей должны зависеть от абстракций. Этот принцип способствует уменьшению связанности в коде и увеличению его гибкости.

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

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

(Single Responsibility Principle, SRP)

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

Основные идеи, связанные с принципом SRP:

  1.  Единственная ответственность: Класс или модуль должен быть ответственным только за одну конкретную часть функциональности или выполнять только одну задачу. Это означает, что класс не должен быть перегружен разными видами ответственностей.
  2.  Легкость поддержки и изменений: При соблюдении SRP, код становится более поддерживаемым и менее подверженным ошибкам. Если необходимо внести изменения в функциональность, они будут ограничены одному классу или модулю, что делает процесс изменения более простым и безопасным.
  3.  Уменьшение связности: Соблюдение SRP помогает уменьшить связность между разными частями программы. Это означает, что изменения в одной части кода не будут сильно влиять на другие части, что улучшает изоляцию функциональности.

Пример SRP в Python:

Предположим, у нас есть класс Order, который отвечает за создание заказов и отправку уведомлений клиентам о статусе заказа. Однако этот класс нарушает SRP, так как он имеет две разные обязанности: обработку заказов и отправку уведомлений. Лучше разделить эти две ответственности на отдельные классы:

class Order:
   def create_order(self, items):
       # Создание заказа

class Notification:
   def send_notification(self, order, message):
       # Отправка уведомления

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

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

Принцип открытости/закрытости

(Open/Closed Principle, OCP)

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

Основные идеи, связанные с принципом OCP:

  1.  Открытость для расширения: Программный компонент (например, класс или модуль) должен предоставлять механизмы для добавления новой функциональности без изменения его собственного исходного кода. Это достигается через использование абстракций, интерфейсов и наследования.
  2.  Закрытость для модификации: Как только компонент разработан и протестирован, его исходный код должен оставаться неизменным при внесении изменений или добавлении новой функциональности. Изменения могут повлиять только на новые компоненты, которые расширяют существующую функциональность.

Пример OCP в Python:

Допустим, у нас есть базовый класс Shape, представляющий геометрическую фигуру, и два класса-наследника Circle и Rectangle. Сначала мы можем создать базовый класс и интерфейс для вычисления площади:

from abc import ABC, abstractmethod

class Shape(ABC):
   @abstractmethod
   def area(self):
       pass

class Circle(Shape):
   def __init__(self, radius):
       self.radius = radius
   
   def area(self):
       return 3.14 * self.radius ** 2

class Rectangle(Shape):
   def __init__(self, width, height):
       self.width = width
       self.height = height
   
   def area(self):
       return self.width * self.height

Теперь, чтобы добавить новую фигуру, мы можем создать новый класс, который также наследует от Shape и реализует метод area. Это демонстрирует открытость для расширения без изменения существующего кода.

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

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

(Liskov Substitution Principle, LSP)

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

Основные идеи, связанные с принципом LSP:

  1.  Согласованное поведение: Все подклассы должны иметь поведение, согласованное с поведением базового класса. Это означает, что методы подклассов должны иметь такую же сигнатуру (типы параметров и возвращаемого значения) и соблюдать те же контракты, что и методы базового класса.
  2.  Безопасность типов: Код, использующий объекты базового класса, должен безопасно работать с объектами любого подкласса без знания о том, какой именно подкласс используется. Это обеспечивает уровень абстракции и гарантирует, что код будет работать корректно, даже если будут внесены изменения или добавлены новые подклассы.
  3.  Исключения и предусловия: Подклассы не должны расширять предусловия (требования к входным данным) методов базового класса и не должны сужать постусловия (гарантии возвращаемых значений). Это означает, что подклассы не могут вводить дополнительные ограничения на входные данные и не могут изменять обещания, сделанные базовым классом.

Пример LSP в Python:

Рассмотрим классы Bird и Ostrich, представляющие птиц. Bird - базовый класс, а Ostrich - подкласс, представляющий страуса:

class Bird:
   def fly(self):
       pass

class Ostrich(Bird):
   def fly(self):
       # Страус не может летать, поэтому переопределяем метод
       raise NotImplementedError("Ostrich can't fly")

Здесь Ostrich переопределяет метод fly, но это делается с учетом согласования с базовым классом Bird. Это гарантирует, что код, который ожидает объекты типа Bird, будет безопасно работать с объектами типа Ostrich.

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

Принцип интерфейсов

(Interface Segregation Principle, ISP)

нацелен на минимизацию зависимостей между компонентами программы.

Основные идеи, связанные с принципом ISP:

  1.  Специфичность интерфейсов: Интерфейсы должны быть специфичными для клиентов, которые их используют. Это означает, что интерфейс должен содержать только те методы, которые нужны конкретному клиенту, и не должен включать в себя методы, которые клиент не использует.
  2.  Множество маленьких интерфейсов: Лучше иметь множество маленьких интерфейсов, чем один большой, охватывающий множество методов. Это позволяет клиентам выбирать только те интерфейсы, которые им нужны, и избегать излишних зависимостей.
  3.  Уменьшение связности: Соблюдение ISP помогает уменьшить связность между компонентами программы, что делает код более гибким и уменьшает вероятность внесения ошибок при изменениях.

Пример ISP в Python:

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

class Worker:
   def work(self):
       pass

Теперь у нас есть два класса: Manager, который работает с сотрудниками, и Robot, который выполняет рутинные задачи. Manager не выполняет работу, а только управляет другими сотрудниками, поэтому он не должен реализовывать метод work. С другой стороны, Robot не управляет сотрудниками, поэтому ему не нужен метод manage.

Мы можем применить принцип ISP и создать два более специфичных интерфейса:

class Workable:
   def work(self):
       pass

class Manageable:
   def manage(self):
       pass

Теперь классы Manager и Robot могут реализовывать только те интерфейсы, которые им нужны, избегая излишних методов и зависимостей:

class Manager(Manageable):
   def manage(self):
       # Реализация управления сотрудниками

class Robot(Workable):
   def work(self):
       # Реализация рутинной работы

Это улучшает ясность и гибкость кода, так как клиенты могут зависеть только от тех интерфейсов, которые им нужны.

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

(Dependency Inversion Principle, DIP)

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

Основные идеи, связанные с принципом DIP:

  1.  Абстракции: Программные компоненты (классы, модули и др.) должны зависеть от абстракций, а не от конкретных реализаций. Абстракции могут представлять собой интерфейсы, абстрактные классы или абстрактные функции.
  2.  Инверсия зависимостей: Зависимости между компонентами должны быть инвертированы. Это означает, что низкоуровневые модули должны зависеть от абстракций, а высокоуровневые модули должны зависеть от этих же абстракций. Таким образом, изменения в низкоуровневых модулях не должны вносить изменения в высокоуровневых модулях.
  3.  Уменьшение связности: Применение DIP помогает уменьшить связность между компонентами, что делает код более гибким и позволяет изолировать изменения в конкретных модулях.

Пример DIP в Python:

Допустим, у вас есть система для отправки уведомлений разными способами, такими как по электронной почте и через SMS. Ваша система включает в себя классы EmailNotification и SMSNotification, которые реализуют отправку уведомлений через электронную почту и SMS соответственно.

Сначала, без применения DIP, вы можете иметь следующий код:

class EmailNotification:
   def send_email(self, message):
       # Логика отправки почты

class SMSNotification:
   def send_sms(self, message):
       # Логика отправки SMS

def send_notification(notification_type, message):
   if notification_type == "email":
       email_notifier = EmailNotification()
       email_notifier.send_email(message)
   elif notification_type == "sms":
       sms_notifier = SMSNotification()
       sms_notifier.send_sms(message)

Здесь функция send_notification зависит от конкретных реализаций уведомлений. Это нарушает DIP, так как высокоуровневая функция send_notification зависит от низкоуровневых классов EmailNotification и SMSNotification.

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

class Notifier:
   def send(self, message):
       pass

class EmailNotification(Notifier):
   def send(self, message):
       # Логика отправки почты

class SMSNotification(Notifier):
   def send(self, message):
       # Логика отправки SMS

def send_notification(notifier, message):
   notifier.send(message)

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

Применение принципов SOLID

Принципы SOLID могут быть применены в разработке на различных языках программирования и фреймворках, включая Python, Django и JavaScript. Вот некоторые примеры применения SOLID в этих контекстах:

Python:

  1.  Принцип единственной ответственности (SRP):
        Создание классов, которые имеют только одну ответственность, например, классы для работы с базой данных и классы для бизнес-логики.
  2.  Принцип открытости/закрытости (OCP):
        Использование наследования и полиморфизма для создания расширяемых компонентов, таких как плагины или расширения.
  3.  Принцип подстановки Барбары Лисков (LSP):
        Обеспечение того, чтобы подклассы корректно реализовывали методы базовых классов, чтобы клиентский код не нарушал принцип LSP.
  4.  Принцип интерфейсов (ISP):
        Создание маленьких и специфических интерфейсов, которые соответствуют потребностям клиентов.
  5.  Принцип инверсии зависимостей (DIP):
        Использование инъекции зависимостей (dependency injection) для создания слабых связей между компонентами и для соблюдения принципа DIP.

Django (Python фреймворк):

  1.  Принцип единственной ответственности (SRP):
        Разделение представлений (views), моделей (models) и шаблонов (templates) для разделения функциональности и улучшения поддерживаемости.
  2.  Принцип открытости/закрытости (OCP):
        Использование Django-плагинов и расширений для добавления новой функциональности без изменения стандартного кода фреймворка.
  3.  Принцип подстановки Барбары Лисков (LSP):
        В Django, это может быть достигнуто через создание подклассов моделей или представлений и соблюдение контрактов фреймворка.
  4.  Принцип интерфейсов (ISP):
        В Django, это может быть применено путем создания кастомных абстрактных моделей или представлений для конкретных задач.
  5.  Принцип инверсии зависимостей (DIP):
        В Django, это может быть применено с использованием встроенной поддержки инъекции зависимостей и создания кастомных классов для управления зависимостями.

JavaScript:

  1.  Принцип единственной ответственности (SRP):
        Разделение функций и классов на небольшие модули, каждый из которых выполняет одну конкретную задачу.
  2.  Принцип открытости/закрытости (OCP):
        Использование паттернов, таких как "Стратегия" или "Плагин", для добавления новой функциональности без изменения существующего кода.
  3.  Принцип подстановки Барбары Лисков (LSP):
        Обеспечение того, чтобы подклассы корректно реализовывали методы базовых классов, чтобы клиентский код не нарушал принцип LSP.
  4.  Принцип интерфейсов (ISP):
        В JavaScript, это может быть достигнуто через создание объектов с определенными методами, которые соответствуют ожидаемым интерфейсам.
  5.  Принцип инверсии зависимостей (DIP):
        Использование инъекции зависимостей и создание слабых связей между компонентами с помощью модулей или фреймворков для соблюдения принципа DIP.

Читайте также:

ChatGPT
Eva
💫 Eva assistant