Блог Михаила | Python | Разработка | Best Practices

"Всем привет! Меня зовут Михаил, я веду Telegram-канал «Python Шпильки», где делюсь изящными приемами программирования. Сегодня хочу показать один из самых полезных паттернов..."

Введение

Представьте: перед вами 200 строк кода, сплошь состоящих из if-elif-else конструкций. Каждый новый condition — еще одна ветка, еще сложнее читать, еще страшнее поддерживать. Знакомо?

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

Проблема: когда условия захватывают код

Допустим, мы пишем обработчик заказов в интернет-магазине:

python

def handle_order(status, order_data):
    if status == "new":
        validate_order(order_data)
        process_payment(order_data)
        send_confirmation_email(order_data)
        
    elif status == "processing":
        check_inventory(order_data)
        assign_to_warehouse(order_data)
        update_tracking(order_data)
        
    elif status == "shipped":
        send_tracking_email(order_data)
        update_analytics(order_data)
        
    elif status == "cancelled":
        process_refund(order_data)
        notify_customer_service(order_data)
        update_inventory(order_data)
        
    elif status == "refunded":
        close_financial_transactions(order_data)
        archive_order(order_data)
        
    # ... и так еще 10-15 статусов

Что здесь не так:

  • ❌ Код растет вертикально с каждым новым статусом

  • ❌ Сложно тестировать отдельные сценарии

  • ❌ При изменении одного статуса нужно перелопачивать всю функцию

  • ❌ Новый разработчик будет разбираться часами

Решение: словарь диспетчеризации функций

А теперь посмотрите на эту же логику, переписанную с помощью паттерна "Словарь диспетчеризации":

python

class OrderProcessor:
    def __init__(self):
        self.handlers = {
            "new": self._handle_new,
            "processing": self._handle_processing,
            "shipped": self._handle_shipped,
            "cancelled": self._handle_cancelled,
            "refunded": self._handle_refunded,
        }
    
    def process_order(self, status, order_data):
        handler = self.handlers.get(status, self._handle_unknown)
        return handler(order_data)
    
    def _handle_new(self, order_data):
        validate_order(order_data)
        process_payment(order_data)
        return send_confirmation_email(order_data)
    
    def _handle_processing(self, order_data):
        check_inventory(order_data)
        assign_to_warehouse(order_data)
        return update_tracking(order_data)
    
    def _handle_shipped(self, order_data):
        send_tracking_email(order_data)
        return update_analytics(order_data)
    
    def _handle_cancelled(self, order_data):
        process_refund(order_data)
        notify_customer_service(order_data)
        return update_inventory(order_data)
    
    def _handle_refunded(self, order_data):
        close_financial_transactions(order_data)
        return archive_order(order_data)
    
    def _handle_unknown(self, order_data):
        logger.warning(f"Unknown order status: {order_data.get('status')}")
        return {"error": "Unknown status"}

# Использование:
processor = OrderProcessor()
result = processor.process_order("new", order_data)

Почему это решение элегантнее?

? Принцип единственной ответственности

Каждый метод отвечает только за один статус. Изменили логику отмены? Правите только handlecancelled.

? Простота тестирования

python

def test_cancelled_order():
    processor = OrderProcessor()
    test_data = {"id": 123, "amount": 100}
    
    # Тестируем только обработку отмены
    result = processor._handle_cancelled(test_data)
    
    assert result["refund_processed"] == True
    mock_notify.assert_called_once()

? Легкое расширение

Добавляем новый статус без изменения существующей логики:

python

def add_preorder_status(self):
    self.handlers["preorder"] = self._handle_preorder

def _handle_preorder(self, order_data):
    reserve_inventory(order_data)
    return send_preorder_confirmation(order_data)

? Визуальная чистота

Теперь архитектура вашего обработчика видна как на ладони:

python

# ВСЯ ЛОГИКА ПРЕДСТАВЛЕНА В ОДНОЙ СТРОКЕ
handlers = {
    "new": self._handle_new,
    "processing": self._handle_processing,
    "shipped": self._handle_shipped,
    # ...
}

Продвинутые возможности

Динамическая регистрация обработчиков

python

def register_handler(self, status, handler):
    self.handlers[status] = handler

# Где-то в другом модуле:
processor.register_handler("preorder", custom_preorder_handler)

Декоратор для автоматической регистрации

python

def register(status):
    def decorator(method):
        method._handler_status = status
        return method
    return decorator

class OrderProcessor:
    def __init__(self):
        self.handlers = {}
        self._auto_register_handlers()
    
    def _auto_register_handlers(self):
        for attr_name in dir(self):
            attr = getattr(self, attr_name)
            if hasattr(attr, '_handler_status'):
                self.handlers[attr._handler_status] = attr
    
    @register("new")
    def handle_new_order(self, order_data):
        # ...

Когда использовать этот паттерн

✅ Идеально подходит для:

  • Обработчиков HTTP-запросов (API роутинг)

  • State-машин и обработки статусов

  • Парсеров различных форматов данных

  • CLI-утилит с множеством команд

❌ Возможно, не стоит использовать:

  • Когда условий всего 2-3 (оверкиллинг)

  • Когда логика слишком простая для вынесения в отдельные функции

  • Когда производительность критична (есть минимальные накладные расходы)

Бенчмарк: если сомневаетесь в производительности

python

import timeit

# if-else подход
def if_else_approach(status):
    if status == "new": return 1
    elif status == "processing": return 2
    # ... 10 условий
    else: return 0

# Словарный подход  
def dict_approach(status):
    return handlers.get(status, lambda: 0)()

handlers = {"new": lambda: 1, "processing": lambda: 2}  # ... 10 условий

# Результаты для 100000 вызовов:
# if-else: 0.015 секунд
# dict: 0.012 секунд

Вывод: Словарный подход часто даже быстрее за счет хэш-таблиц!

Заключение

Паттерн "Словарь диспетчеризации" — это не просто синтаксический сахар. Это:

  • Архитектурное решение для сложной логики

  • Инструмент рефакторинга для борьбы с code smell

  • Демонстрация мастерства в Python

Следующие шаги:

  1. Найдите в своем коде самый страшный if-elif-else блок

  2. Попробуйте переписать его с помощью словаря функций

  3. Поделитесь результатами в комментариях!

А в следующей статье разберем, как комбинировать этот подход с декораторами для создания настоящих произведений искусства в коде.


Теги: PythonРефакторингПаттерны проектированияBest PracticesКод качество

Понравилась статья? Подписывайтесь на мой Telegram-канал Python Шпильки — там регулярно выходят такие же изящные решения для повседневных задач разработчика.

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


  1. pda0
    13.10.2025 11:21

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

    Немного примеров match/case в Python 3.10

    Существует уже 4 года. Не благодарите.

    Меня зовут Михаил, я веду Telegram-канал «Python Шпильки», где делюсь изящными приемами программирования.

    Спасибо, запомнил. Ни в коем случае не подписываться.


    1. Valberton
      13.10.2025 11:21

      Немного примеров match/case в Python 3.10

      Существует уже 4 года. Не благодарите.

      А если у меня проект для 2.7 Пайтона, будет ли резонно использовать этот метод, вместо въевшегося if-else?


      1. qss53770
        13.10.2025 11:21

        А если у меня не python 2.7, а проект на перфо картах?


      1. pda0
        13.10.2025 11:21

        А если на Cobol? Короче говоря, если вы поддерживаете проект на python 2, поддержка которого прекратилась в 2020 и который сейчас не поставляется в дистрибутивах, то вы достаточно опытный разработчик, чтобы вручную возиться со всем этим старьём и вам не нужны такие советы, сами способны додуматься.

        А вот показывать такое новичкам как "изящные решения для повседневных задач разработчика", пожалуйста, не надо.


  1. vadimr
    13.10.2025 11:21

    Очень много синтаксического мусора в виде низачем не нужных имён методов из-за отсутствия в питоне многострочных лямбд.


  1. outlingo
    13.10.2025 11:21

    Когда не знал про паттерн "стратегия", но умеешь пользоваться LLM и генерировать ИИ-шит


    1. AlexanderMelory
      13.10.2025 11:21

      то, что он класс не назвал Strategy, не означает, что он не реализовал данный паттерн лол))

      у автора как раз таки и реализован паттерн стратегия, с некоторыми нюансами, но все же, для примера вполне достаточно.

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

      с чем вы тут не согласны?)))


      1. outlingo
        13.10.2025 11:21

        Мне не нравится тем, что это AI-slop и LLM-shit

        Но если предположить, что публикатор все же писал статью "вручную"...

        Если ты пишешь тег "Паттерны проектирования" - то сошлись, что в этих самых паттернах такой подход называется стратегией.

        Вообще, многие разработчики на питоне имеют какую-то антипатию к патернам, аппелируя к тому, что паттерны переусложнены и в них много лишнего - вот смотрите, как можно просто сделать, "тяп-ляп и в продакшн". В результате получается то, что получается - код, насквозь прошитый глобальными переменными, статическими сессиями и глобальными функциями с сайд-эффектами. Да-да, весь этот ад с reserve_inventory(...) - откуда у нее возьмется сессия, в которой она сходит в базу или другой микросервис? Или notify_customer_service(...) - откуда в нем возьмутся реквизиты для доступа к очередям рассылки уведомлений?

        Такие простые реализации, которые смело копипастит из ответов LLM автор - без всей этой замученности с типами, классами и прочим - они в рабочей схеме не прокатят. Код хорошо бы прогонять через проверку статической типизации - вот эти вот все from typing import .... и mypy (это вообще категорическое требование - с моей точки зрения, код без них вообще недопустим, кроме случаев безжалостного легаси). А когда привозится статическая типизация, вот тогда и начинается самое интересное - то есть то, чего начинающие разработчики не любят.

        Ну а то, что показано в статье - поытка разделить god method - но в результате просто превращает его в god object. Но LLM на это пофиг, чушь на входе - чушь на выходе, обучили на спагетти из статиков, глобальных переменных и сайдэффектов - получите то же самое в ответ на свой запрос. И следующую итерацию обучат на этом же генеративе. Идеальная самоодебиливающаяся система, которая учится быть еще тупее на своем же выхлопе.


        1. AlexanderMelory
          13.10.2025 11:21

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


        1. Petr_axeman
          13.10.2025 11:21

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

          Просто в нашей любимой замке паттерны буквально переусложненны. Недавно неплохая статья была на хабре, что-то вроде "Почему паттерны проектирования в питоне не работают". Очень интересная статья которая очень точно описывает часть нюансов отношения змеи и паттернов.

          Общий посыл статьи примерно такой - "Зачем шаблоны, если можно и лучше без шаблонов". То что указанное в статье является каким то шаблоном вообще удивление. В некоторых случаях в питоне можно callable метать и по спискам, и интегрироваться по ним. И использовать как замыкания.


          1. outlingo
            13.10.2025 11:21

            Шаблон это подход, принцип реализации задачи. Детали могут отличаться, но суть одна и та же - создается посредник, в котором регистрируются исполнители (стратегии). Принятие решения о том какому исполнителю выполнить действие (какую стратегию использовать) принимает посредник. Вместо прямого вызова исполнителя вызывается посредник.

            И далее начинаются уже детали реализации, например будут ли посредники Callable (питон), делегатами (C#) или объектами реализующими интерфейс (Java), будет ли выбор реального актора базироваться на словаре, или стратегии будут предоставлять предикаты которые определяют готовность стратегии обработать вызов, или какой-нибудь другой способ. Много вариантов.

            То, что автор не любит слово "шаблон" не значит, что его тут нет :-)


  1. evgenyk
    13.10.2025 11:21

    В случае чуть более-менее сложных логик, описываемый подход (с использованием словаря обработчиков, где значения - обработчики, а ключи - строки воходных логических параметров) все равно значительно лучше, чем как match/case, так и паттерна стратегия, ИМХО. Таким подходом еще деды на ассемблере пользовались. Он прост и надежен, как АК-47.


  1. Lecron
    13.10.2025 11:21

    Если есть обоснованное предположение, что структура функций не будет меняться, либо более сложная логика появится только в редких статус-обработчиках, можно еще больше упростить.

    class OrderProcessor:
        def __init__(self):
            self.handlers = {
                "new": [validate_order, process_payment, send_confirmation_email],
                 ...
            }
        
        def __call__(self, order_data):
            status = order_data['status']
            if status in self.handlers:
                for handler in self.handlers[status]:
                    reuslt = handler(order_data)
            else:
                logger.warning(f"Unknown order status: {status}")
                result = {"error": "Unknown status"}
            return result
    


  1. encore-show
    13.10.2025 11:21

    а теперь можно просто выкладывать сгенерированный текст ИИ просто в качестве статьи и называть это Best Practices?

    Это же просто копипаст ответа курсора)


    1. LeonidPr
      13.10.2025 11:21

      Этот период надо просто пережить, а аккаунты тех, кто постит эту субстанцию, банить нещадно.

      Со временем народ поймет, что это бесполезно.


      1. encore-show
        13.10.2025 11:21

        это факт, просто становится грустно наблюдать что AI который должен вроде как давать знания, так как это доступ к такому объему знаний, а по факту я смотрю как только становится больше глупых и перестающих думать.
        Но наверное да, просто нужно время


  1. python_pins_mikhail Автор
    13.10.2025 11:21

    Спасибо за комментарии! Тапок у меня теперь … немерено. Я предложил один из вариантов обхода if-elif-else, и попытался описать этот подход как можно понятнее. Согласен что есть и другие подходы, которые вы написали, за это вам отдельное спасибо!!! Пусть каждый выбирает себе по душе.


    1. GuriKo
      13.10.2025 11:21

      Да гори в аду. Не благодари.


  1. K1nGsmaN
    13.10.2025 11:21

    очередной AI-slop. зачем вы с этой ерундой на хабр лезете? кого вы здесь хотите обмануть?

    даже не удосужился сообщить, что это обычный паттерн «стратегия». видимо уровень автора - выдавать ответы чата за свои.