Принципы 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.