Принципы SOLID

SOLID — это пять принципов объектно-ориентированного программирования, которые помогают писать чистый, гибкий и поддерживаемый код.

S. Принцип единственной ответственности (Single Responsibility Principle — SRP)

подробнее

“Один класс — одна задача.”

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

Пример:
❌ Плохо: Класс User сохраняет свои данные в базу и отправляет email.
✅ Хорошо: Класс User хранит данные, UserRepository сохраняет в базу, EmailService отправляет письма.


❌ Не SOLID: Класс User отвечает и за хранение данных, и за сохранение в БД, и за отправку email.

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save_to_db(self):
        print(f"Сохранение пользователя {self.name} в базу данных")

    def send_email(self, message):
        print(f"Отправка письма {message} на {self.email}")

✅ SOLID: Разделяем ответственность на три класса.

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    def save_to_db(self, user):
        print(f"Сохранение пользователя {user.name} в базу данных")

class EmailService:
    def send_email(self, user, message):
        print(f"Отправка письма {message} на {user.email}")

Почему лучше:

  • Изменение логики сохранения или отправки писем не затрагивает класс User.
  • Код проще тестировать и поддерживать.

O. Принцип открытости/закрытости (Open/Closed Principle — OCP)

подробнее

“Код должен быть открыт для расширения, но закрыт для изменений.”

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

Пример:
❌ Плохо: Менять метод calculateArea() для каждого нового типа фигуры.
✅ Хорошо: У каждой фигуры (Circle, Square) свой метод calculateArea(), который вызывается через общий интерфейс.


❌ Не SOLID: При добавлении новой фигуры нужно изменять метод calculate_area().

class AreaCalculator:
    def calculate_area(self, shape):
        if isinstance(shape, Rectangle):
            return shape.width * shape.height
        elif isinstance(shape, Circle):
            return 3.14 * shape.radius ** 2
        # Добавлять новые условия для новых фигур

✅ SOLID: Каждая фигура реализует свой метод area(), и AreaCalculator не нужно изменять.

from abc import ABC, abstractmethod

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

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

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

class AreaCalculator:
    def calculate_area(self, shape: Shape):
        return shape.area()

Почему лучше:

  • Новые фигуры (Triangle, Square) можно добавлять, не меняя AreaCalculator.

L. Принцип подстановки Барбары Лисков (Liskov Substitution Principle — LSP)

подробнее

“Подклассы должны заменять родительские классы без ошибок.”

Если есть класс Птица, и от него наследуется Пингвин, то пингвин не должен ломать логику, если у птицы есть метод летать().

Пример:
❌ Плохо: У Пингвин есть метод летать(), но он не работает.
✅ Хорошо: Вынести летать() в отдельный интерфейс ЛетающаяПтица.


❌ Не SOLID: Подкласс Penguin нарушает логику родителя Bird.

class Bird:
    def fly(self):
        print("Птица летит")

class Penguin(Bird):
    def fly(self):
        raise Exception("Пингвины не умеют летать!")  # Нарушение LSP

✅ SOLID: Разделяем интерфейсы, чтобы пингвин не зависел от fly().

class Bird:
    pass

class FlyingBird(Bird):
    def fly(self):
        print("Птица летит")

class Penguin(Bird):
    def swim(self):
        print("Пингвин плавает")

Почему лучше:

  • Теперь Penguin не нарушает контракт родительского класса.

I. Принцип разделения интерфейса (Interface Segregation Principle — ISP)

подробнее

“Лучше много маленьких интерфейсов, чем один большой.”

Класс не должен зависеть от методов, которые он не использует.

Пример:
❌ Плохо: Интерфейс Worker с методами work(), eat(), sleep() — не все работники должны их реализовывать.
✅ Хорошо: Разделить на Workable, Eatable, Sleepable.


❌ Не SOLID: Один интерфейс Worker с избыточными методами.

from abc import ABC, abstractmethod

class Worker(ABC):
    @abstractmethod
    def work(self):
        pass

    @abstractmethod
    def eat(self):
        pass

class Robot(Worker):
    def work(self):
        print("Робот работает")

    def eat(self):  # Роботу не нужно есть!
        raise NotImplementedError("Роботы не едят!")

✅ SOLID: Разделяем интерфейсы.

class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Eatable(ABC):
    @abstractmethod
    def eat(self):
        pass

class Human(Workable, Eatable):
    def work(self):
        print("Человек работает")

    def eat(self):
        print("Человек ест")

class Robot(Workable):
    def work(self):
        print("Робот работает")

Почему лучше:

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

D. Принцип инверсии зависимостей (Dependency Inversion Principle — DIP)

подробнее

“Зависи от абстракций, а не от конкретики.”

Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций (интерфейсов).

Пример:
❌ Плохо: Класс ReportGenerator напрямую зависит от MySQLDatabase.
✅ Хорошо: ReportGenerator зависит от интерфейса Database, а MySQLDatabase его реализует.


❌ Не SOLID: Класс ReportGenerator зависит от конкретной БД.

class MySQLDatabase:
    def fetch_data(self):
        print("Данные из MySQL")

class ReportGenerator:
    def __init__(self):
        self.db = MySQLDatabase()  # Жёсткая зависимость

    def generate_report(self):
        data = self.db.fetch_data()
        print("Генерация отчёта на основе", data)

✅ SOLID: Зависим от абстракции (Database), а не от конкретной реализации.

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def fetch_data(self):
        pass

class MySQLDatabase(Database):
    def fetch_data(self):
        print("Данные из MySQL")

class PostgreSQLDatabase(Database):
    def fetch_data(self):
        print("Данные из PostgreSQL")

class ReportGenerator:
    def __init__(self, db: Database):  # Принимает любой Database
        self.db = db

    def generate_report(self):
        data = self.db.fetch_data()
        print("Генерация отчёта на основе", data)

Почему лучше:

  • Можно легко подменить БД (например, на PostgreSQL), не меняя ReportGenerator.