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

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

Комментарии (12)


  1. HemulGM
    03.06.2024 04:26
    +5

    https://habr.com/ru/articles/465507/

    SRP - это не когда у тебя один класс имеет только один метод.


    1. Andy_U
      03.06.2024 04:26
      +1

      Да и тот @classmethod


      1. Ryav
        03.06.2024 04:26
        +1

        Зачем тогда вообще этот класс? )


  1. rukhi7
    03.06.2024 04:26
    +2

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

    Типа есть принципиальная разница реализации этого принципа в зависимости от языка? Принцып как-то по другому действует для классов написанных в С++ или в Java?

    Это реклама Python?


  1. Tinkz
    03.06.2024 04:26
    +3

    сомнительный способ, осталось только все классы по разным файлам распихать


  1. orchanin
    03.06.2024 04:26
    +3

    Периодически попадаются статьи по SOLID и примерно понимал этот принцип как вы описали в статье (только конечно не ограничиваясь одной функцией в классе). Но недвано добрался до первоисточника, в котором написано совсем иное и был крайне удивлен как много опытных программистов ошибаются в понимании принципа единой ответственности.

    Цитата из книги: Роберт Мартин "Чистая архитектура. Искусство разработки программного обеспечения"

    Hidden text

    Из всех принципов SOLID наиболее трудно понимаемым является принцип единственной ответственности (Single Responsibility Principle, SRP). Это, вероятно, обусловлено выбором названия, недостаточно точно соответствующего сути. Услышав это название, многие программисты решают: оно означает, что каждый модуль должен отвечать за что‑то дно. Самое интересное, что такой принцип действительно существует.

    Он гласит: функция должна делать что‑то одно и только одно. Этот принцип мы используем, когда делим большие функции на меньшие, то есть на более низком уровне. Но он не является одним из принципов SOLID — это не принцип единственной ответственности.

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

    Пользователи и заинтересованные лица как раз и есть та самая «причина для изменения», о которой говорит принцип. Фактически принцип можно перефразировать так: Модуль должен отвечать за одного пользователя или заинтересованное лицо. и только за одного К сожалению, слова «пользователь» и «заинтересованное лицо» не совсем правильно использовать здесь, потому что одного и того же изменения системы могут желать несколько пользователей или заинтересованных лиц.

    Более правильным выглядит понятие группы, состоящей из одного или нескольких лиц, желающих данного изменения. Мы будем называть такие группы акторами (actor).


    1. DenSigma
      03.06.2024 04:26
      +1

      Более того, я когда читал про solid, всегда вызывало отторжение. Всегда было чувство, что это модная чепуха. Много противоречий, разнотолков у авторов. Вплоть до того, что в классах оставляли только один метод invoke.

      Потом я прочитал Мартина, дядю Боба. А у него все просто и понятно. SOLID - попытка бездумной формализации в нескольких предложениях философии Мартина. Имхо - довольно неудачная.

      Кстати, и Spring необходимо изучать только после того, как прочитаешь "Желтую книгу". Иначе он все равно вызовет у тебя отторжение.


      1. Andrey_Solomatin
        03.06.2024 04:26

        Кстати, и Spring необходимо ...


        Он тут вообще не кстати.


  1. DenSigma
    03.06.2024 04:26

    Статья хорошая. Однако данный пример может ложно навести на мысль, что необходимо разбивать на классы с одним-единственным методом. А это не так, согласно Мартину.

    Предлагаю или явно это указать, либо сделать классы, к примеру не Saver, а DataSource и определить в нем методы Save и Find.


    1. Andrey_Solomatin
      03.06.2024 04:26

      Это статья о декомпозиции. SRP он о том когда и как нужно применять декомпозицию.


  1. titan_pc
    03.06.2024 04:26
    +1

    Классы в python - это оверхед на ram. Нафиг ООП.

    Солидный код - плохой код.

    Солидный сервис - хороший сервис


    1. Andrey_Solomatin
      03.06.2024 04:26

      Классы в python - это оверхед на ram. Нафиг ООП.


      Классы это оверхед по памяти в любом языке.

      ООП это удобный инструмент со своими ограничениями.

      Солидный код - плохой код.

      Если вы про код использующий SOLID принципы, это не так. Под копотом там достаточно сильные и универсальные идеи. Не везде они применими и не всякий код который который пытается быть SOLID им является.

      Солидный сервис - хороший сервис

      Не знаком с этим термином.