Computer science Python Tech Программирование

SOLID – теория с примерами

SOLID – это набор из пяти основных принципов объектно-ориентированного программирования, предложенный Робертом Мартином. Эти принципы помогают создавать максимально понятные, легко поддерживаемые и расширяемые программы.

Что означает аббревиатура SOLID

  • S – Single Responsibility Principle – принцип единственной ответственности.
  • O – Open-Closed Principle – принцип открытости-закрытости
  • L – Liskov Substitution Principle – принцип подстановки Барбары Лисков
  • I – Interface Segregation Principle – принцип разделения интерфейсов
  • D – Dependency Inversion Principle – принцип инверсии зависимостей

Связь с ООП

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

  • S – Single Responsibility Principle – классы и методы
  • O – Open-Closed Principle – наследование и полиморфизм
  • L – Liskov Substitution Principle – наследование и правильная иерархия классов
  • I – Interface Segregation Principle – интерфейсы, абстрактные классы, множественное наследование
  • D – Dependency Inversion Principle – абстрактные классы и интерфейсы, внедрение зависимостей

Single Responsibility Principle

Принцип единственной ответственности. Класс должен иметь только одну причину для изменения. Класс должен решать только одну задачу.

Это обеспечивает удобство сопровождения и тестирования. А изменения в одной части программы не затрагивают другие.

Пример правильного использования

Здесь каждый класс решает одну конкретную задачу, поэтому его легко менять, поддерживать и тестировать.

class UserManager:
    def create_user(self, username):
        pass

class EmailSender:
    def send_email(self, email, message):
        pass

Пример нарушения принципа

Здесь класс объединяет две разные функциональности – управление пользователем и отправка сообщений.

class UserManager:
    def create_user(self, username):
        pass

    def send_email(self, email, message):
        pass

Open-Closed Principle

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

Программные сущности (классы, модули) должны быть открыты для расширения и закрыты для изменений.

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

Пример правильного использования

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

class DiscountCalculator:
    def calculate(self):
        return 0.1

class VipDiscountCalculator(DiscountCalculator):
    def calculate(self):
        return 0.2

Пример нарушения принципа

Здесь при добавлении нового типа клиентов придется менять метод.

class DiscountCalculator:
    def calculate(self, customer_type):
        if customer_type == "regular":
            return 0.1
        elif customer_type == "vip":
            return 0.2

Liskov Substitution Principle

Принцип подстановки Барбары Лисков. Этот принцип довольно труден для восприятия. Его удобно разобрать на нескольких примерах (см. ниже). Принцип гласит:

Объекты дочерних классов должны корректно заменять объекты родительского класса без изменения корректности работы программы.

Это обеспечивает правильное переопределение методов и наследование классов. Устраняет неожиданные ошибки при замене объектов одного класса другим.

Пример кода для задачи управления разными видами автомобилей.

class Vehicle:
    """Это родительский класс, задающий общие методы для дочерних классов"""

    def start_engine(self):
        print("Двигатель запущен")

    def stop_engine(self):
        print("Двигатель остановлен")

    def drive(self):
        raise NotImplementedError("Этот метод должен быть реализован в дочернем классе")


class Car(Vehicle):
    """Дочерний класс, переопределяет метод drive под легковой автомобиль"""

    def drive(self):
        print("Легковой автомобиль едет быстро и плавно")


class Truck(Vehicle):
    """Дочерний класс, переопределяет метод drive под грузовой автомобиль"""

    def drive(self):
        print("Грузовик едет медленно, но перевозит большой груз")


class Bus(Vehicle):
    """Дочерний класс, переопределяет метод drive под автобус"""

    def drive(self):
        print("Автобус едет по маршруту и останавливается на остановках")


# Запустим все доступные методы для дочерних классов, включая методы из родительского класса
vehicles = [Car(), Truck(), Bus()]
for vehicle in vehicles:
    print(vehicle.__class__.__name__)
    vehicle.start_engine()
    vehicle.drive()
    vehicle.stop_engine()
    print()


# Output

# Car
# Двигатель запущен
# Легковой автомобиль едет быстро и плавно
# Двигатель остановлен
# 
# Truck
# Двигатель запущен
# Грузовик едет медленно, но перевозит большой груз
# Двигатель остановлен
# 
# Bus
# Двигатель запущен
# Автобус едет по маршруту и останавливается на остановках
# Двигатель остановлен

Зачем вообще нужен родительский класс, если его заменяет дочерний?

Ответ: родительский класс задает общий интерфейс и общее поведение для всех дочерних классов. Если такого интерфейса не требуется или у классов нет общих элементов поведения, то им не нужен родительский класс.

Interface Segregation Principle

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

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

Пример правильной реализации

class Printer:
    def print_document(self):
        pass

class Scanner:
    def scan_document(self):
        pass

class MultiFunctionPrinter(Printer, Scanner):
    pass

Пример неправильной реализации

Если у нас будет принтер, который не может сканировать, его объекту придется реализовывать ненужный метод scan_document.

class Printer:
    def print_document(self):
        pass

    def scan_document(self):
        pass

Dependency Inversion Principle

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

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

Пример правильной реализации принципа

# Этот родительский класс не зависит от реализации дочерних классов
class Database:
    def save(self, data):
        pass


class MySQLDatabase(Database):
    def save(self, data):
        print("Сохраняем в MySQL")


class PostgreSQLDatabase(Database):
    def save(self, data):
        print("Сохраняем в PostgresSQL")


# Этот класс зависит от абстракции Database
# В нем легко менять одну базу данных на другую
# Добавление новой базы данных (нового класса) не потребует изменений в UserManager
class UserManager:
    def __init__(self, db: Database):
        self.db = db


mysql = MySQLDatabase()
pgsql = PostgreSQLDatabase()
um = UserManager(mysql)

print(um.db.__class__.__name__)

Пример неправильной реализации

class MySQLDatabase:
    def save(self, data):
        print("Сохраняем в MySQL")

# Здесь UserManager жестко зависит от конкретной реализации базы данных 
class UserManager:
    def __init__(self):
        self.db = MySQLDatabase()

Вставить формулу как
Блок
Строка
Дополнительные настройки
Цвет формулы
Цвет текста
#333333
Используйте LaTeX для набора формулы
Предпросмотр
\({}\)
Формула не набрана
Вставить