Фундамент чистого кода: пять принципов SOLID, DRY, KISS, YAGNI. Как писать поддерживаемый и расширяемый код на Python.
Чистый код — это не роскошь, а необходимость для поддерживаемой системы
Представьте: вы добавляете новую фичу, и ломается код в трёх других местах. Вы исправляете одно — появляется другое. Знакомо? Это признак нарушения принципов проектирования.
SOLID — это пять принципов, которые помогают писать код, который:
Принцип единственной ответственности: класс должен иметь только одну причину для изменения.
class User:
def __init__(self, id, name, email):
self.id = id
self.name = name
self.email = email
def save(self, db_connection):
"""Сохраняет пользователя в БД"""
db_connection.execute(
"INSERT INTO users VALUES (?, ?, ?)",
(self.id, self.name, self.email)
)
def send_welcome_email(self, smtp_server):
"""Отправляет приветственное письмо"""
message = f"Welcome, {self.name}!"
smtp_server.send(self.email, message)
def validate_email(self):
"""Проверяет корректность email"""
return "@" in self.email and "." in self.emailПроблема: класс User отвечает за:
Изменение логики БД потребует изменения класса User, даже если бизнес-логика не менялась.
class User:
"""Модль данных — только хранение"""
def __init__(self, id, name, email):
self.id = id
self.name = name
self.email = email
class UserRepository:
"""Работа с БД — единственная ответственность"""
def __init__(self, db_connection):
self._db = db_connection
def save(self, user: User):
self._db.execute(
"INSERT INTO users VALUES (?, ?, ?)",
(user.id, user.name, user.email)
)
class EmailService:
"""Отправка email — единственная ответственность"""
def __init__(self, smtp_server):
self._smtp = smtp_server
def send_welcome(self, user: User):
message = f"Welcome, {user.name}!"
self._smtp.send(user.email, message)
class EmailValidator:
"""Валидация — единственная ответственность"""
@staticmethod
def is_valid(email: str) -> bool:
return "@" in email and "." in emailВыгоды:
Принцип открытости/закрытости: сущности должны быть открыты для расширения, но закрыты для модификации.
class PaymentProcessor:
def process(self, payment_type: str, amount: float):
if payment_type == "credit_card":
self._process_credit_card(amount)
elif payment_type == "paypal":
self._process_paypal(amount)
elif payment_type == "crypto":
self._process_crypto(amount)
# При добавлении нового типа придётся менять этот метод
else:
raise ValueError(f"Unknown payment type: {payment_type}")
def _process_credit_card(self, amount): ...
def _process_paypal(self, amount): ...
def _process_crypto(self, amount): ...Проблема: каждый новый тип оплаты требует изменения существующего кода → риск сломать работающее.
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
"""Абстракция — открыта для расширения"""
@abstractmethod
def process(self, amount: float) -> bool:
pass
class CreditCardPayment(PaymentMethod):
def __init__(self, card_number, cvv):
self.card_number = card_number
self.cvv = cvv
def process(self, amount: float) -> bool:
# Логика обработки кредитной карты
return True
class PayPalPayment(PaymentMethod):
def __init__(self, email):
self.email = email
def process(self, amount: float) -> bool:
# Логика PayPal
return True
class CryptoPayment(PaymentMethod):
def __init__(self, wallet_address):
self.wallet_address = wallet_address
def process(self, amount: float) -> bool:
# Логика криптовалюты
return True
class PaymentProcessor:
"""Закрыт для модификации — работает с абстракцией"""
def process(self, method: PaymentMethod, amount: float) -> bool:
return method.process(amount)Выгоды:
Принцип подстановки Барбары Лисков: объекты дочерних классов должны быть заменяемы объектами родительского класса без нарушения корректности программы.
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
self._height = value
def area(self):
return self._width * self._height
class Square(Rectangle):
"""Нарушает LSP: меняет поведение setter'ов"""
@Rectangle.width.setter
def width(self, value):
self._width = value
self._height = value # !
@Rectangle.height.setter
def height(self, value):
self._width = value # !
self._height = value
# Клиентский код ломается:
def double_area(rectangle: Rectangle):
rectangle.width *= 2
rectangle.height *= 2
return rectangle.area()
# Для Rectangle: area увеличится в 4 раза
# Для Square: area увеличится только в 2 раза!class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width, height):
self._width = width
self._height = height
def area(self) -> float:
return self._width * self._height
class Square(Shape):
def __init__(self, side):
self._side = side
def area(self) -> float:
return self._side ** 2
# Теперь client code работает корректно с любыми Shape
def print_area(shape: Shape):
print(f"Area: {shape.area()}")Выгоды:
Принцип разделения интерфейса: клиенты не должны зависеть от методов, которые они не используют.
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def sleep(self):
pass
class HumanWorker(Worker):
def work(self): ...
def eat(self): ...
def sleep(self): ...
class RobotWorker(Worker):
def work(self): ...
def eat(self):
raise NotImplementedError("Robots don't eat!") # !
def sleep(self):
raise NotImplementedError("Robots don't sleep!") # !Проблема: RobotWorker вынужден реализовывать ненужные методы.
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 HumanWorker(Workable, Eatable, Sleepable):
def work(self): ...
def eat(self): ...
def sleep(self): ...
class RobotWorker(Workable):
def work(self): ...
# Только нужные интерфейсыВыгоды:
Принцип инверсии зависимостей: модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
class MySQLDatabase:
def connect(self): ...
def execute(self, query): ...
class UserService:
def __init__(self):
self._db = MySQLDatabase() # Прямая зависимость от конкретики
def get_user(self, user_id):
self._db.execute(f"SELECT * FROM users WHERE id = {user_id}")Проблема:
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self): ...
@abstractmethod
def execute(self, query): ...
class MySQLDatabase(Database):
def connect(self): ...
def execute(self, query): ...
class PostgreSQLDatabase(Database):
def connect(self): ...
def execute(self, query): ...
class InMemoryDatabase(Database):
"""Для тестов"""
def __init__(self):
self._data = {}
def connect(self): ...
def execute(self, query):
return self._data.get(query)
class UserService:
def __init__(self, db: Database): # Зависимость от абстракции
self._db = db
def get_user(self, user_id):
return self._db.execute(f"SELECT * FROM users WHERE id = {user_id}")Dependency Injection (внедрение зависимости):
# В production
db = MySQLDatabase()
service = UserService(db)
# В тестах
db = InMemoryDatabase()
service = UserService(db)Выгоды:
Каждая знания должна иметь единственное, непротиворечивое представление в системе.
# Плохо
def get_active_users():
return User.objects.filter(status='active', is_deleted=False)
def count_active_users():
return User.objects.filter(status='active', is_deleted=False').count()
# Хорошо
ACTIVE_USER_FILTER = {'status': 'active', 'is_deleted': False}
def get_active_users():
return User.objects.filter(**ACTIVE_USER_FILTER)
def count_active_users():
return User.objects.filter(**ACTIVE_USER_FILTER).count()Избегайте излишней сложности.
# Слишком умно
result = functools.reduce(lambda acc, x: acc + x['value'], filter(lambda x: x['active'], items), 0)
# Просто и понятно
total = sum(item['value'] for item in items if item['active'])Не добавляйте функциональность, пока она не понадобится.
# Не надо заранее
class UserService:
def get_user(self, user_id): ...
def get_user_cached(self, user_id): ... # Пока не нужно
def get_user_from_replica(self, user_id): ... # Пока не нужноЗадача: Рефакторинг нарушающего SOLID кода
Дан класс ReportGenerator:
class ReportGenerator:
def generate_pdf_report(self, data, output_path):
# Генерация PDF
pass
def generate_csv_report(self, data, output_path):
# Генерация CSV
pass
def send_email(self, to, report_path):
# Отправка email
pass
def save_to_database(self, data):
# Сохранение в БД
pass
def validate_data(self, data):
# Валидация
passЗадание:
| Принцип | Суть | Выгода |
|---|---|---|
| SRP | Одна причина для изменения | Легче понимать и менять |
| OCP | Открыт для расширения, закрыт для модификации | Безопасное добавление функциональности |
| LSP | Наследники заменяемы родителем | Надёжный полиморфизм |
| ISP | Много специализированных интерфейсов | Нет лишних зависимостей |
| DIP | Зависимость от абстракций | Слабая связанность, тестируемость |
SOLID — не догма, а руководство. Иногда можно отступить, но делайте это осознанно.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.