Реализация принципа единственной ответственности на Python

2e1e13a68a86ab18c071bc1b23b5cd6f.png

Привет, Хабр!

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

  1. Pylint помогает анализировать код на наличие ошибок и несоответствий стилю, а также выявляет нарушения принципов SOLID, включая SRP.

    pylint mymodule.py
  2. Mypy — статический анализатор типов для Python, который помогает обнаруживать типовые ошибки и улучшать структуру кода.

    mypy mymodule.py
  3. Pytest помогает создавать модульные тесты для каждого отдельного компонента.

    def test_order_validator():
        validator = OrderValidator()
        assert validator.validate({'product_id': 123, 'quantity': 1})
        assert not validator.validate({'product_id': 123, 'quantity': 0})
  4. Dataclasses модуль позволяет создавать классы данных, которые следуют SRP, отделяя логику данных от поведения.

    from dataclasses import dataclass
    
    @dataclass
    class Order:
        product_id: int
        quantity: int

Про другие архитектурные принципы и инструменты коллеги из OTUS рассказывают в рамках практических онлайн-курсов. Также хочу напомнить о том, что в календаре мероприятий вы можете зарегистрироваться на ряд интересных и абсолютно бесплатных вебинаров.

© Habrahabr.ru