Принципы SOLID в Python
Пять принципов, которые отличают код, который можно читать, от кода, который хочется удалить
Содержание
Пять принципов
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 менее строгий - если класс просто не реализует ненужный метод и не падает, формально ничего не сломано.