Принципы SOLID — это пять рекомендаций по проектированию, которые помогают разработчикам писать сопровождаемый, гибкий и масштабируемый код. Понимание и применение этих принципов отделяет код профессионального уровня от быстрых «костылей». В этом руководстве рассматривается практическое применение SOLID с точки зрения senior-разработчика.
Почему SOLID важен
Следование принципам SOLID приводит к:
- Сопровождаемости: код проще изменять, не ломая существующее поведение
- Тестируемости: компоненты можно тестировать изолированно
- Гибкости: новые функции требуют меньше рефакторинга
- Читаемости: ясные ответственности и интерфейсы
- Совместной работе: команды могут работать над отдельными компонентами
S — Принцип единственной ответственности (SRP)
Определение: у класса должна быть одна и только одна причина для изменения.
Плохой пример
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
# Логика работы с базой данных
db.execute("INSERT INTO users...")
def send_welcome_email(self):
# Логика email
smtp.send(self.email, "Welcome!")
def generate_report(self):
# Логика формирования отчётов
return f"User Report: {self.name}"
У этого класса три причины для изменения: данные пользователя, операции с базой данных, отправка email и формирование отчётов.
Хороший пример
class User:
"""Only handles user data."""
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
"""Handles user persistence."""
def save(self, user: User):
db.execute("INSERT INTO users...", user.name, user.email)
def find_by_email(self, email: str) -> User:
row = db.query("SELECT * FROM users WHERE email = ?", email)
return User(row['name'], row['email'])
class EmailService:
"""Handles email sending."""
def send_welcome(self, user: User):
smtp.send(user.email, "Welcome!", self._welcome_template(user))
def _welcome_template(self, user: User) -> str:
return f"Hello {user.name}, welcome to our platform!"
class UserReportGenerator:
"""Handles user reports."""
def generate(self, user: User) -> str:
return f"User Report: {user.name}, Email: {user.email}"
Теперь у каждого класса одна ответственность, и их можно изменять независимо.
O — Принцип открытости/закрытости (OCP)
Определение: классы должны быть открыты для расширения, но закрыты для модификации.
Плохой пример
class PaymentProcessor:
def process(self, payment_type: str, amount: float):
if payment_type == "credit_card":
# Логика работы с кредитной картой
return self._process_credit_card(amount)
elif payment_type == "paypal":
# Логика PayPal
return self._process_paypal(amount)
elif payment_type == "bitcoin":
# Логика Bitcoin
return self._process_bitcoin(amount)
# Добавление нового способа оплаты требует модификации этого класса
Каждый новый способ оплаты требует модификации обработчика.
Хороший пример
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
"""Abstract base class for payment methods."""
@abstractmethod
def process(self, amount: float) -> bool:
pass
class CreditCardPayment(PaymentMethod):
def __init__(self, card_number: str, cvv: str):
self.card_number = card_number
self.cvv = cvv
def process(self, amount: float) -> bool:
# Логика обработки платежа по кредитной карте
return gateway.charge(self.card_number, amount)
class PayPalPayment(PaymentMethod):
def __init__(self, email: str):
self.email = email
def process(self, amount: float) -> bool:
# Логика обработки платежа через PayPal
return paypal.charge(self.email, amount)
class BitcoinPayment(PaymentMethod):
def __init__(self, wallet_address: str):
self.wallet_address = wallet_address
def process(self, amount: float) -> bool:
# Логика обработки платежа через Bitcoin
return bitcoin.transfer(self.wallet_address, amount)
class PaymentProcessor:
"""Processor is closed for modification, open for extension."""
def process(self, method: PaymentMethod, amount: float) -> bool:
return method.process(amount)
# Использование: добавление нового способа оплаты не требует изменения PaymentProcessor
class ApplePayPayment(PaymentMethod):
def process(self, amount: float) -> bool:
return apple_pay.charge(amount)
Новые способы оплаты можно добавлять, не затрагивая существующий код.
L — Принцип подстановки Лисков (LSP)
Определение: подклассы должны быть взаимозаменяемы со своими базовыми классами без нарушения корректности программы.
Плохой пример
class Rectangle:
def __init__(self, width: int, height: int):
self._width = width
self._height = height
def set_width(self, width: int):
self._width = width
def set_height(self, height: int):
self._height = height
def area(self) -> int:
return self._width * self._height
class Square(Rectangle):
def set_width(self, width: int):
self._width = width
self._height = width # Нарушает LSP!
def set_height(self, height: int):
self._width = height # Нарушает LSP!
self._height = height
def calculate_area(rect: Rectangle):
rect.set_width(5)
rect.set_height(4)
assert rect.area() == 20 # Не работает для Square!
Square изменяет поведение, унаследованное от Rectangle, нарушая взаимозаменяемость.
Хороший пример
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
def print_area(shape: Shape):
print(f"Area: {shape.area()}") # Работает для любой Shape
# Оба работают корректно
print_area(Rectangle(5, 4)) # Площадь: 20
print_area(Square(5)) # Площадь: 25
I — Принцип разделения интерфейсов (ISP)
Определение: клиентов не следует вынуждать зависеть от интерфейсов, которые они не используют.
Плохой пример
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def sleep(self):
pass
class Human(Worker):
def work(self):
print("Working")
def eat(self):
print("Eating")
def sleep(self):
print("Sleeping")
class Robot(Worker):
def work(self):
print("Working")
def eat(self):
pass # Роботы не едят — вынуждены реализовывать
def sleep(self):
pass # Роботы не спят — вынуждены реализовывать
Хороший пример
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Sleepable(ABC):
@abstractmethod
def sleep(self):
pass
class Human(Workable, Eatable, Sleepable):
def work(self):
print("Human working")
def eat(self):
print("Human eating")
def sleep(self):
print("Human sleeping")
class Robot(Workable):
def work(self):
print("Robot working")
# Фабрика, которой нужны только рабочие
def assign_work(workers: list[Workable]):
for worker in workers:
worker.work()
# Работает с обоими
assign_work([Human(), Robot()])
Клиенты зависят только от тех интерфейсов, которые им действительно нужны.
D — Принцип инверсии зависимостей (DIP)
Определение: высокоуровневые модули не должны зависеть от низкоуровневых модулей. И те, и другие должны зависеть от абстракций.
Плохой пример
class MySQLDatabase:
def connect(self):
return mysql.connect("localhost", "user", "pass")
def query(self, sql: str):
return self.connect().execute(sql)
class UserService:
def __init__(self):
self.db = MySQLDatabase() # Прямая зависимость от MySQL
def get_user(self, user_id: int):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
UserService жёстко связан с MySQL — нельзя легко сменить базу данных.
Хороший пример
from abc import ABC, abstractmethod
class Database(ABC):
"""Abstraction that both high and low-level modules depend on."""
@abstractmethod
def query(self, sql: str):
pass
@abstractmethod
def execute(self, sql: str, params: tuple):
pass
class MySQLDatabase(Database):
def __init__(self, host: str, user: str, password: str):
self.connection = mysql.connect(host, user, password)
def query(self, sql: str):
return self.connection.execute(sql)
def execute(self, sql: str, params: tuple):
return self.connection.execute(sql, params)
class PostgreSQLDatabase(Database):
def __init__(self, host: str, user: str, password: str):
self.connection = psycopg2.connect(host=host, user=user, password=password)
def query(self, sql: str):
return self.connection.cursor().execute(sql)
def execute(self, sql: str, params: tuple):
return self.connection.cursor().execute(sql, params)
class UserService:
def __init__(self, database: Database): # Зависит от абстракции
self.db = database
def get_user(self, user_id: int):
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
# Легко переключать реализации
mysql_db = MySQLDatabase("localhost", "user", "pass")
user_service = UserService(mysql_db)
# Или использовать PostgreSQL без изменения UserService
postgres_db = PostgreSQLDatabase("localhost", "user", "pass")
user_service = UserService(postgres_db)
# Или замокать для тестирования
class MockDatabase(Database):
def query(self, sql: str):
return [{"id": 1, "name": "Test User"}]
def execute(self, sql: str, params: tuple):
pass
test_service = UserService(MockDatabase())
Полный пример: система обработки заказов
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List
# Доменные модели (SRP)
@dataclass
class Product:
id: str
name: str
price: float
@dataclass
class Order:
id: str
products: List[Product]
@property
def total(self) -> float:
return sum(p.price for p in self.products)
# Абстракции (DIP)
class OrderRepository(ABC):
@abstractmethod
def save(self, order: Order) -> None:
pass
@abstractmethod
def find(self, order_id: str) -> Order:
pass
class NotificationService(ABC):
@abstractmethod
def notify(self, message: str, recipient: str) -> None:
pass
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount: float, payment_details: dict) -> bool:
pass
# Реализации (OCP — можно добавлять новые реализации)
class DatabaseOrderRepository(OrderRepository):
def save(self, order: Order) -> None:
db.execute("INSERT INTO orders...")
def find(self, order_id: str) -> Order:
return db.query("SELECT * FROM orders WHERE id = ?", order_id)
class EmailNotificationService(NotificationService):
def notify(self, message: str, recipient: str) -> None:
smtp.send(recipient, "Order Update", message)
class SMSNotificationService(NotificationService):
def notify(self, message: str, recipient: str) -> None:
twilio.send(recipient, message)
class StripePaymentGateway(PaymentGateway):
def charge(self, amount: float, payment_details: dict) -> bool:
return stripe.charge(amount, payment_details['card_token'])
# Высокоуровневый сервис (DIP — зависит от абстракций)
class OrderService:
def __init__(
self,
repository: OrderRepository,
notification: NotificationService,
payment: PaymentGateway
):
self.repository = repository
self.notification = notification
self.payment = payment
def place_order(self, order: Order, payment_details: dict, customer_email: str) -> bool:
if self.payment.charge(order.total, payment_details):
self.repository.save(order)
self.notification.notify(
f"Order {order.id} confirmed! Total: ${order.total}",
customer_email
)
return True
return False
# Внедрение зависимостей (composition root)
def create_order_service() -> OrderService:
return OrderService(
repository=DatabaseOrderRepository(),
notification=EmailNotificationService(),
payment=StripePaymentGateway()
)
# Использование
service = create_order_service()
order = Order("123", [Product("1", "Widget", 29.99)])
service.place_order(order, {"card_token": "tok_xxx"}, "[email protected]")
Ключевые выводы
- SRP: один класс — одна ответственность
- OCP: расширяйте через абстракции, а не через модификации
- LSP: подклассы должны соблюдать контракты родительских классов
- ISP: небольшие, сфокусированные интерфейсы вместо больших
- DIP: зависите от абстракций, внедряйте зависимости
Принципы SOLID — не жёсткие правила, а рекомендации, которые повышают качество кода при вдумчивом применении. Начните с выявления «запахов кода», затем выполняйте рефакторинг в сторону этих принципов постепенно.