Рубрики
Без рубрики

Понимание цепочки ответственности

Простая и легкая иллюстрация того, что такое и как использовать шаблон цепочки ответственности с помощью Python. Помечено с рисунком, учебником, Python.

Во многих случаях на наших проектах мы находимся в пишу кусочки кода, которые полностью останавливают программу или продолжать учитывать определенное условие, это может быть валидация данных, которые были отправлены, запрос в базе данных, или логическая проверка нашего Бизнес-правила, чтобы решить каждую проверку, мы склонны писать простым, если во многих случаях он хорошо подходит Но также начинает генерировать большую проблему, которая имеет кучу IFS на том же месте, сделает код сложнее для отладки и чтения.

Я собираюсь использовать в качестве примера в качестве примера электронной коммерции, которая продает продукт со скидкой на определенную дату, шаги, которые нам нужно проверить: это продукт на складе? Скидка все еще действует (действительный период)? Значение дисконтирования превышает цену продукта?

Чтобы решить эту проблему, мы обычно пишем много, если это, как это:

def show_discount(product):
    if product.stock == 0:
        return f"there's no stock for {product}"

    discount = Discount.find(product.id)
    if discount.expire_at < today:
        return f"discount for {product} is expired"

    product_price = product.price - discount_amount.value
    if product_price <= 0:
        return "cannot apply discount, discount value is greater than the product price"

    return f"total with discount: {product_price}"

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

  1. Связь
  2. Сложность
  3. Организация

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

blocked_brands = ("apple",)
def show_discount(product):
    if product.stock == 0:
        return f"there's no stock for {product}"

    if product.brand in blocked_brands:
        return "cannot apply discount for blocked brands"

    # + previous code...

Добавьте новый, если сможете решить проблему в самый быстрый и простой способ, но она начнет создавать другую проблему «кодового запаха», наша функция теперь даже дольше и сложнее, эта ситуация может сделать код сложнее, но если «SimpleSt» решение не является правильным решением, как мы можем улучшить/исправить эту ситуацию?

Цепочка ответственности – это поведенческий дизайн, который позволяет вам передавать запросы по цепочке обработчиков. Получив вызов, каждый обработчик решает либо обработать запрос, либо передать его следующему обработчику в цепочке.

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

Объявите интерфейс обработчика и опишите подпись метода для обработки запросов. Чтобы удалить дублированный код, мы можем написать абстрактные классы, полученные из интерфейса обработчика.

class Handler(ABC):
    @abstractmethod
    def set_next(self, handler: Handler) -> Handler:
        pass

    @abstractmethod
    def handle(self, request) -> Optional[str]:
        pass


class AbstractHandler(Handler):
    _next_handler: Handler = None

    def set_next(self, handler: Handler) -> Handler:
        self._next_handler = handler
        # Returning a handler from here will let us link handlers in a
        # convenient way:
        # product.set_next(brand).set_next(discount)
        return handler

    @abstractmethod
    def handle(self, product: Any) -> str:
        if self._next_handler:
            return self._next_handler.handle(product)
        return None

Для каждого обработчика мы создаем обработчик подкласса, который реализует методы интерфейса. Каждый обработчик должен принимать свои собственные решения отдельно.

class ProductHandler(AbstractHandler):
    def handle(product) -> str:
        if product.stock == 0:
            return "there's no stock for {product}"

        return super().handle(product)

class BrandHandler(AbstractHandler):
    _blocked_brands = ("apple",)

    def handle(product) -> str:
        if product.brand in self._blocked_brands:
            return f"{product.name} is not allowed to have discount, blocked by brand"

        return super().handle(product)

class DiscountHandler(AbstractHandler):
    def handle(product) -> str:
        discount = Discount.find(product.id)
        if not has_valid_period(discount):
            return "the discount is expired"

        if has_valid_amount(product.price, discount_amount.value):
            return "cannot apply discount, discount value is greater than the product price"

        return super().handle(product)

    def has_valid_period(discount):
        return discount.expires_at < datetime.now()

    def has_valid_amount(product_price, discount_value):
        return discount_value < product_price

Что творится?

У каждого класса Handler есть атрибут, который говорит, что является следующим шагом

ProductHandler._next_handler will be BrandHandler 
BrandHandler._next_handler will be DiscountHandler 
DiscountHandler.next_handler will be None

Рефакторинг

Теперь, когда мы отделили обязанности, используя классы, которые реализуют интерфейс, мы можем изменить наш код:

product_handler = ProductHandler()
brand_handler = BrandHandler()
discount_handler = DiscountHandler()

product_handler.set_next(brand_handler).set_next(discount_handler)

# product sample
product = Product(id=1, name="mobile phone", stock=0, brand="sansumg", price=1000)

print(product_handler.handle(product))

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

# will execute only BrandHandler and DiscountHandler
print(brand_handler.handle(product))

Если нам нужно добавить новый обработчик, мы можем просто создать другой класс и добавить другой метод Set_Next.

class NotificationHandler(AbstractHandler):
    def handle(product) -> str:
        if product.stock <= 10 and product.price >= 1000:
            logger.info(f"{product.name} is running out of stock, current stock: {product.stock}")

        return super().handle(product)
product_handler = ProductHandler()
brand_handler = BrandHandler()
discount_handler = DiscountHandler()
notification_handler = NotificationHandler()

product_handler.set_next(
    brand_handler
).set_next(
    discount_handler
).set_next(
    notification_handler
)

Рекомендации

https://refactoring.guru/pt-br/design-patterns/chain-of-responsibility

Оригинал: “https://dev.to/rafaacioly/understanding-chain-of-responsability-pattern-59ob”