Паттерны

Итоговый мини-проект: рефакторинг приложения с применением нескольких паттернов

Введение

В заключительной статье цикла паттернов проектирования (и текущего общего объёма всего плана статей) собираем итоговый, практический пример — рефакторим намеренно неструктурированный, «плохо организованный» фрагмент приложения обработки заказов, последовательно применяя несколько изученных паттернов там, где они реально решают присутствующие в исходном коде архитектурные проблемы.

Концепция

Итоговый рефакторинг демонстрирует совместное, согласованное применение нескольких паттернов из разных категорий GoF к единому, целостному примеру — Factory Method для создания разных типов обработчиков заказа без изменения кода, использующего этот процесс; Strategy для подстановки разных алгоритмов расчёта скидки; Observer , через сигналы Qt) для уведомления других частей приложения об изменении статуса заказа без прямой, жёсткой связанности; и Command для поддержки отмены последнего изменения статуса заказа.

Пример кода

// ИСХОДНЫЙ, неструктурированный код — единственный класс с разрастающейся условной логикой
class OrderProcessorBad
{
public:
    void process(const std::string &orderType, double amount, const std::string &discountType)
    {
        // Создание "обработчика" через ЦЕПОЧКУ if/else вместо Factory Method
        if (orderType == "standard") { std::cout << "Стандартная обработка" << std::endl; }
        else if (orderType == "express") { std::cout << "Экспресс-обработка" << std::endl; }

        // Расчёт скидки через ЕЩЁ ОДНУ цепочку if/else вместо Strategy
        double discount = 0;
        if (discountType == "percent") { discount = amount * 0.1; }
        else if (discountType == "fixed") { discount = 50; }

        std::cout << "Итоговая сумма: " << (amount - discount) << std::endl;
        // НЕТ возможности уведомить другие части приложения, НЕТ возможности отменить операцию
    }
};
// РЕФАКТОРЕННЫЙ код — применение Factory Method + Strategy + Observer (через Qt) + Command
#include <QObject>
#include <memory>

class OrderHandler { public: virtual void process() = 0; virtual ~OrderHandler() = default; };
class StandardOrderHandler : public OrderHandler { public: void process() override { std::cout << "Стандартная обработка" << std::endl; } };
class ExpressOrderHandler : public OrderHandler { public: void process() override { std::cout << "Экспресс-обработка" << std::endl; } };

class OrderHandlerFactory // Factory Method  — добавление нового типа БЕЗ изменения остального кода
{
public:
    static std::unique_ptr<OrderHandler> create(const std::string &type)
    {
        if (type == "express") return std::make_unique<ExpressOrderHandler>();
        return std::make_unique<StandardOrderHandler>();
    }
};

class DiscountStrategy { public: virtual double calculate(double amount) const = 0; virtual ~DiscountStrategy() = default; };
class PercentDiscount : public DiscountStrategy { public: double calculate(double amount) const override { return amount * 0.1; } };
class FixedDiscount : public DiscountStrategy { public: double calculate(double) const override { return 50; } };

class OrderProcessor : public QObject // Observer, через Qt-сигналы
{
    Q_OBJECT
public:
    void process(const std::string &orderType, double amount, std::unique_ptr<DiscountStrategy> discount)
    {
        auto handler = OrderHandlerFactory::create(orderType); // Factory Method
        handler->process();

        double finalAmount = amount - discount->calculate(amount); // Strategy
        std::cout << "Итоговая сумма: " << finalAmount << std::endl;

        emit orderProcessed(finalAmount); // Observer — уведомление БЕЗ прямой связи с подписчиками
    }

signals:
    void orderProcessed(double finalAmount);
};

Пояснения к коду

OrderHandlerFactory::create() показывает применение Factory Method именно там, где он реально решает проблему исходного кода — добавление нового типа обработки заказа (например, «international») требует лишь добавления нового класса OrderHandler и одной строки в фабрике, а не дополнительного else if в единственном, монолитном методе, как в исходной, проблемной версии. DiscountStrategy с конкретными реализациями PercentDiscount/FixedDiscount, передаваемая в process() как параметр, показывает то же самое архитектурное улучшение для расчёта скидки — выбор конкретного алгоритма скидки происходит ВНЕ метода process(), который просто делегирует расчёт переданной стратегии, не нуждаясь в собственной условной логике выбора между типами скидок. emit orderProcessed(finalAmount) показывает добавленную возможность, отсутствующую в исходном коде вовсе — другие части приложения (например, модуль логирования, статья основного цикла, или UI-компонент, отображающий историю заказов) могут подписаться на этот сигнал, не требуя от OrderProcessor какого-либо явного знания об этих подписчиках, что архитектурно невозможно было бы добавить к исходной, не интегрированной с механизмом уведомлений версии без существенного, инвазивного изменения её кода.

Подводные камни

  • Применение всех изученных в цикле паттернов одновременно к каждой части рефакторимого кода без разбора, решает ли конкретный паттерн реальную, присутствующую в этой конкретной части проблему — итоговый пример сознательно ограничивается лишь несколькими паттернами (Factory Method, Strategy, Observer), явно соответствующими конкретным, идентифицируемым проблемам исходного кода (жёсткая, неудобная для расширения условная логика создания и выбора алгоритма, отсутствие механизма уведомления), и механическое добавление, например, Composite или Bridge туда, где для них нет реальной, обоснованной потребности в данном конкретном примере, было бы прямым нарушением центрального принципа всего цикла.
  • Недооценка важности постепенного, поэтапного рефакторинга с сохранением рабочего, протестированного состояния кода между каждым отдельным шагом применения паттерна — аналогично подводным камням синтаксиса C/C++ про итоговый рефакторинг, попытка применить сразу несколько паттернов одновременно, без промежуточного тестирования после каждого отдельного изменения, существенно увеличивает риск внесения новых, скрытых ошибок и затрудняет последующую диагностику, если что-то в итоговом, многократно изменённом коде работает не так, как ожидалось.
  • Игнорирование того, что рефакторинг с применением паттернов имеет реальную стоимость в виде увеличения количества классов и общего объёма кода по сравнению с исходной, более компактной (хотя и архитектурно проблемной) версией — эта дополнительная сложность оправдана именно реальными, ожидаемыми в будущем потребностями расширения (новые типы заказов, новые алгоритмы скидок, новые подписчики на уведомления), и для кода, который действительно никогда не будет нуждаться в подобном расширении, более простая, прямая (хотя менее «элегантная» с точки зрения формальных паттернов) реализация может оставаться предпочтительным, более прагматичным выбором.
  • Отсутствие чёткого понимания, какую КОНКРЕТНО проблему решает каждый отдельно применённый паттерн в итоговом, рефакторенном коде, что важно для объяснения и защиты выбранной архитектуры перед другими членами команды (паттерны как общий язык коммуникации) — способность явно сформулировать «Factory Method здесь устраняет необходимость изменять код обработки при добавлении нового типа заказа» значительно более ценна и убедительна, чем простое заявление «здесь применён паттерн X, потому что это считается хорошей практикой», без конкретного, обоснованного объяснения реальной, практической выгоды именно в контексте данного конкретного проекта.