Принципы SOLID в Python

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

Принципы SOLID в Python.md

Содержание


Пять принципов

SOLID - это пять принципов проектирования объектно-ориентированного кода. Аббревиатура из первых букв:

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

SOLID - это пять принципов проектирования кода, сформулированных Робертом Мартином. Каждая буква - отдельный принцип. S - у класса одна причина меняться, O - код расширяется без правки существующего, L - подклассы не ломают поведение родителя, I - интерфейсы дробятся под конкретные нужды, D - модули зависят от абстракций, а не от конкретных реализаций. На практике SOLID помогает писать код, который легко тестировать, расширять и не бояться трогать.


Простыми словами

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


S - Single Responsibility (Одна ответственность)

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

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

Правило простое: если ты описываешь класс и используешь слово «и» - скорее всего, класс делает слишком много.


O - Open/Closed (Открыт/Закрыт)

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

Представь розетку. Ты не вскрываешь стену каждый раз, когда хочешь подключить новый прибор. Просто вставляешь вилку. Код должен работать так же - новое поведение добавляется снаружи, а не через правку существующего кода.


L - Liskov Substitution (Подстановка Лисков)

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

Звучит очевидно, но ловушка здесь частая: пингвин - это птица, но летать не умеет. Если базовый класс Птица умеет летать, а у пингвина этот метод бросает ошибку - принцип нарушен.


I - Interface Segregation (Разделение интерфейсов)

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

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


D - Dependency Inversion (Инверсия зависимостей)

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

Представь выключатель на стене. Ему всё равно, что за лампочка вкручена - светодиодная или накаливания. Выключатель работает с абстракцией «лампочка», а не с конкретной моделью. Если лампочка сгорит - ты просто вкрутишь другую, не меняя проводку.


Код

S - Single Responsibility

# ПЛОХО - класс делает слишком много
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def get_user_info(self):
        return f"{self.name} {self.email}"

    def save_to_db(self):         # работа с БД
        ...

    def send_welcome_email(self): # отправка почты
        ...


# ХОРОШО - каждый класс делает одно дело
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def get_user_info(self):
        return f"{self.name} {self.email}"


class UserRepository:
    def save(self, user: User):
        ...  # только работа с БД


class EmailService:
    def send_welcome(self, user: User):
        ...  # только отправка почты

O - Open/Closed

# ПЛОХО - чтобы добавить новую скидку, нужно лезть внутрь метода
class Discount:
    def apply(self, user_type, price):
        if user_type == "vip":
            return price * 0.8
        elif user_type == "student":
            return price * 0.9
        # каждый новый тип = правка этого кода


# ХОРОШО - новое поведение добавляется снаружи
class Discount:
    def apply(self, price):
        return price


class VipDiscount(Discount):
    def apply(self, price):
        return price * 0.8


class StudentDiscount(Discount):
    def apply(self, price):
        return price * 0.9

# Нужна новая скидка - просто создаём новый класс, старый не трогаем

L - Liskov Substitution

# ПЛОХО - Penguin нарушает поведение базового класса
class Bird:
    def fly(self):
        return "Лечу!"


class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("Пингвины не летают")  # сюрприз!


def make_bird_fly(bird: Bird):
    print(bird.fly())  # упадёт с Penguin


# ХОРОШО - разделяем поведение честно
class Bird:
    def eat(self):
        return "Ем"


class FlyingBird(Bird):
    def fly(self):
        return "Лечу!"


class Penguin(Bird):    # пингвин - птица, но не летающая
    def swim(self):
        return "Плыву!"

I - Interface Segregation

# ПЛОХО - один огромный интерфейс
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def fly(self): ...

    @abstractmethod
    def swim(self): ...

    @abstractmethod
    def run(self): ...


class Dog(Animal):
    def fly(self):
        pass  # собака не летает, но метод реализовывать обязана


# ХОРОШО - узкие интерфейсы под конкретные умения
class Flyable(ABC):
    @abstractmethod
    def fly(self): ...


class Swimmable(ABC):
    @abstractmethod
    def swim(self): ...


class Duck(Flyable, Swimmable):  # утка умеет и то, и то
    def fly(self): return "Лечу!"
    def swim(self): return "Плыву!"


class Dog(Swimmable):             # собака только плавает
    def swim(self): return "Плыву!"

D - Dependency Inversion

# ПЛОХО - верхний уровень жёстко привязан к конкретной реализации
class MySQLDatabase:
    def save(self, data):
        print(f"Сохраняю в MySQL: {data}")


class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # жёсткая привязка, не заменить без правки

    def create_user(self, data):
        self.db.save(data)


# ХОРОШО - зависим от абстракции
from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def save(self, data): ...


class MySQLDatabase(Database):
    def save(self, data):
        print(f"Сохраняю в MySQL: {data}")


class PostgreSQLDatabase(Database):
    def save(self, data):
        print(f"Сохраняю в PostgreSQL: {data}")


class UserService:
    def __init__(self, db: Database):  # зависит от абстракции
        self.db = db

    def create_user(self, data):
        self.db.save(data)


# Подменить БД - дело одной строки
service = UserService(db=PostgreSQLDatabase())

Глубже

SOLID придумал Роберт Мартин (он же «Дядя Боб») в начале 2000-х. Принципы не специфичны для Python - они работают в любом объектно-ориентированном языке, просто в Python выглядят немного иначе из-за утиной типизации.

Почему не всегда нужно следовать SOLID буквально. Иногда разбить один маленький класс на три ради «принципа единственной ответственности» - это оверинжиниринг. SOLID - это направление мышления, а не свод законов. Правило простое: если класс сложно тестировать или сложно объяснить его назначение одним предложением - скорее всего, он нарушает S.

Самый часто нарушаемый принцип на практике - S. Классы обрастают методами постепенно, и никто специально не планирует их раздувать. За этим просто нужно следить при ревью.

Самый сложный для понимания - L. Проблема не в том, что подкласс делает что-то запрещённое, а в том, что он не выполняет обещания, данные базовым классом. Нарушение Лисков почти всегда значит, что иерархия наследования выстроена неправильно.

В Python нет интерфейсов в классическом смысле, как в Java. Их роль выполняют абстрактные классы (ABC) или просто утиная типизация. Поэтому принцип I в Python менее строгий - если класс просто не реализует ненужный метод и не падает, формально ничего не сломано.


FAQ

SOLID - это только для больших проектов? Нет, но в маленьких проектах его нарушения просто не так болезненны. Чем больше проект и чем больше людей над ним работает - тем сильнее нарушения SOLID бьют по скорости разработки. Писать по SOLID с первого дня - хорошая привычка, даже на учебных проектах.
Нужно ли знать SOLID на джуниор-позицию? Знать и уметь объяснить - да, это ожидается. Уметь идеально применять во всём коде - нет, на это уходят годы практики. Достаточно понимать каждый принцип и привести простой пример.
Чем SOLID отличается от паттернов проектирования? SOLID - это принципы мышления, общие правила. Паттерны проектирования (Фабрика, Стратегия, Декоратор и т.д.) - это конкретные решения конкретных задач. Паттерны часто реализуют SOLID-принципы, но это разные уровни абстракции.
Почему в Python нет интерфейсов, как в Java? Потому что Python использует утиную типизацию - «если объект ходит как утка и крякает как утка, значит это утка». Формального контракта интерфейса нет. Вместо него используют `ABC` (Abstract Base Class) из модуля `abc` или просто договорённости в команде. С появлением `Protocol` из модуля `typing` (Python 3.8+) ситуация стала ещё гибче.
Как запомнить все пять принципов? Через образы - одна ответственность (повар не бухгалтер), расширяй без правки (розетка), подкласс не ломает родителя (пингвин не птица в смысле полётов), узкие интерфейсы (пульт с нужными кнопками), зависи от абстракции (выключатель не знает тип лампочки). После пары прочитанных статей и одного написанного примера - запоминается само.