Привет, Хабр!
Сегодня мы рассмотрим одну из основополагающих концепций SOLID-принципов — принцип единственной ответственности или сокращенно - SRP. Разберем, что такое SRP и как правильно его применять в Python.
Принцип единственной ответственности гласит, что каждый класс, метод или модуль должен иметь только одну причину для изменения. Проще говоря, каждый компонент вашей системы должен отвечать только за одну функциональность. Т.е если вам нужно внести изменение, связанное с этой функциональностью, вам придется изменить только один компонент.
Когда каждый класс или модуль выполняет одну четко определенную задачу, становится гораздо проще понять его назначение и взаимодействие с другими частями системы.
Что будет, если не соблюдать SRP?
Если класс или модуль берет на себя несколько обязанностей, это приводит к увеличению сложности кода. Такой код сложнее читать, понимать и поддерживать. Также, когда один класс выполняет несколько задач, изменение в одной из них может непредсказуемо повлиять на другие.
Классы, которые нарушают SRP, обычно плохо масштабируются и трудно переиспользуются. Их невозможно легко адаптировать для других целей или проектов.
Примеры реализации
Для начала рассмотрим класс, который нарушает принцип единственной ответственности. Представим себе класс UserManager
, который одновременно отвечает за создание юзера, валидацию данных и сохранение юзера в БД:
class UserManager:
def __init__(self, username, email):
self.username = username
self.email = email
def create_user(self):
if self.validate_email(self.email):
self.save_to_database()
print(f'User created: {self.username}, {self.email}')
else:
print(f'Invalid email: {self.email}')
def validate_email(self, email):
return "@" in email # простой пример валидации
def save_to_database(self):
# логика сохранения пользователя в базу данных
print(f'User saved to database: {self.username}, {self.email}')
# пример
user_manager = UserManager('IVAN', 'john@example.com')
user_manager.create_user()
Класс нарушает SRP, т.к выполняет несколько задач: валидацию email, создание пользователя и сохранение его в базу данных.
Для исправления нарушения SRP нужно разделить обязанности на отдельные классы: User
, UserValidator
, UserDatabase
, и UserCreator
. Каждый класс будет отвечать только за одну задачу:
class User:
def __init__(self, username, email):
self.username = username
self.email = email
class UserValidator:
def validate_email(self, email):
return "@" in email # простой пример валидации
class UserDatabase:
def save_user(self, user):
# логика сохранения пользователя в базу данных
print(f'User saved to database: {user.username}, {user.email}')
class UserCreator:
def __init__(self, validator, database):
self.validator = validator
self.database = database
def create_user(self, username, email):
user = User(username, email)
if self.validator.validate_email(email):
self.database.save_user(user)
print(f'User created: {username}, {email}')
else:
print(f'Invalid email: {email}')
# пример
validator = UserValidator()
database = UserDatabase()
creator = UserCreator(validator, database)
creator.create_user('IVAN', 'john@example.com')
Теперь каждый класс отвечает за одну конкретную задачу, что соответствует принципу единственной ответственности.
Рассмотрим другой пример, обработку заказов в интернет-магазине. Изначально есть класс, который нарушает SRP, т.к он одновременно обрабатывает заказ, валидирует данные и отправляет уведомления:
class OrderManager:
def __init__(self, order):
self.order = order
def process_order(self):
if self.validate_order(self.order):
self.save_order_to_database()
self.send_notification()
print(f'Order processed: {self.order}')
else:
print('Invalid order')
def validate_order(self, order):
# простая валидация заказа
return order["quantity"] > 0
def save_order_to_database(self):
# логика сохранения заказа в базу данных
print(f'Order saved to database: {self.order}')
def send_notification(self):
# логика отправки уведомления
print(f'Notification sent for order: {self.order}')
# пример
order = {"product_id": 123, "quantity": 1}
order_manager = OrderManager(order)
order_manager.process_order()
Рефакторинг этого класса для соответствия SRP:
class Order:
def __init__(self, product_id, quantity):
self.product_id = product_id
self.quantity = quantity
class OrderValidator:
def validate(self, order):
# простая валидация заказа
return order.quantity > 0
class OrderDatabase:
def save(self, order):
# логика сохранения заказа в базу данных
print(f'Order saved to database: {order}')
class NotificationService:
def send(self, message):
# логика отправки уведомления
print(f'Notification sent: {message}')
class OrderProcessor:
def __init__(self, validator, database, notifier):
self.validator = validator
self.database = database
self.notifier = notifier
def process_order(self, order):
if self.validator.validate(order):
self.database.save(order)
self.notifier.send(f'Order processed: {order}')
print(f'Order processed: {order}')
else:
print('Invalid order')
# пример
order = Order(product_id=123, quantity=1)
validator = OrderValidator()
database = OrderDatabase()
notifier = NotificationService()
processor = OrderProcessor(validator, database, notifier)
processor.process_order(order)
Инструменты и методологии для SRP
Фасадный паттерн
Фасадный паттерн помогает упростить взаимодействие между сложными подсистемами, предоставляя простой интерфейс для клиента. С фасадом можно скрыть сложность подсистем и предоставлять единый интерфейс для взаимодействия с ними.
Предположим, есть система обработки заказов, включающая несколько классов для управления заказами, оплатами и уведомлениями. Без фасадного паттерна клиенту пришлось бы взаимодействовать с каждым из этих классов напрямую:
class OrderManager:
def process_order(self, order):
print(f'Processing order: {order}')
class PaymentProcessor:
def process_payment(self, payment):
print(f'Processing payment: {payment}')
class NotificationService:
def send_notification(self, message):
print(f'Sending notification: {message}')
# клиентский код без фасадного паттерна
order = 'Order123'
payment = 'Payment123'
order_manager = OrderManager()
payment_processor = PaymentProcessor()
notifier = NotificationService()
order_manager.process_order(order)
payment_processor.process_payment(payment)
notifier.send_notification(f'Order processed: {order}')
А с использованием фасадного паттерна все будет выглядеть так:
class OrderFacade:
def __init__(self):
self.order_manager = OrderManager()
self.payment_processor = PaymentProcessor()
self.notifier = NotificationService()
def process_order(self, order, payment):
self.order_manager.process_order(order)
self.payment_processor.process_payment(payment)
self.notifier.send_notification(f'Order processed: {order}')
# клиентский код с фасадным паттерном
order = 'Order123'
payment = 'Payment123'
order_facade = OrderFacade()
order_facade.process_order(order, payment)
Интерфейсы и абстрактные классы
Интерфейсы и абстрактные классы помогают разделить обязанности и четко определить контракт, который должен реализовать класс.
Создание интерфейсов для валидации, сохранения и уведомления:
from abc import ABC, abstractmethod
class Validator(ABC):
@abstractmethod
def validate(self, data):
pass
class Saver(ABC):
@abstractmethod
def save(self, data):
pass
class Notifier(ABC):
@abstractmethod
def notify(self, message):
pass
class OrderValidator(Validator):
def validate(self, order):
return order.get('quantity', 0) > 0
class OrderSaver(Saver):
def save(self, order):
print(f'Saving order: {order}')
class OrderNotifier(Notifier):
def notify(self, message):
print(f'Sending notification: {message}')
# использование интерфейсов
order = {'product_id': 123, 'quantity': 1}
validator = OrderValidator()
saver = OrderSaver()
notifier = OrderNotifier()
if validator.validate(order):
saver.save(order)
notifier.notify('Order processed successfully')
Разделяем обязанности на интерфейсы, что позволяет каждому классу реализовывать только свои специфические методы, соответствующие SRP.
Библиотеки Python, поддерживающие SRP
Для поддержки SRP и других принципов SOLID в Python можно использовать различные библиотеки.
Pylint помогает анализировать код на наличие ошибок и несоответствий стилю, а также выявляет нарушения принципов SOLID, включая SRP.
pylint mymodule.py
Mypy - статический анализатор типов для Python, который помогает обнаруживать типовые ошибки и улучшать структуру кода.
mypy mymodule.py
Pytest помогает создавать модульные тесты для каждого отдельного компонента.
def test_order_validator(): validator = OrderValidator() assert validator.validate({'product_id': 123, 'quantity': 1}) assert not validator.validate({'product_id': 123, 'quantity': 0})
Dataclasses модуль позволяет создавать классы данных, которые следуют SRP, отделяя логику данных от поведения.
from dataclasses import dataclass @dataclass class Order: product_id: int quantity: int
Про другие архитектурные принципы и инструменты коллеги из OTUS рассказывают в рамках практических онлайн-курсов. Также хочу напомнить о том, что в календаре мероприятий вы можете зарегистрироваться на ряд интересных и абсолютно бесплатных вебинаров.