Запах цепочки if-else

В прошивке и настольном C++ накапливается этот паттерн:

1float filter(float sample, FilterType type) {
2    if      (type == FilterType::None)     return sample;
3    else if (type == FilterType::LowPass)  return lowPass(sample);
4    else if (type == FilterType::Median)   return median(sample);
5    else if (type == FilterType::Kalman)   return kalman(sample);
6    return sample;
7}

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

Паттерн «стратегия» заменяет цепочку на заменяемый объект поведения. Вызывающий выбирает, какую стратегию использовать; алгоритм инкапсулирован внутри неё.


Стратегия времени выполнения — виртуальный вызов

Классическая реализация GoF: абстрактный базовый класс определяет интерфейс, конкретные классы реализуют его, контекст хранит указатель.

 1// Интерфейс — что делает фильтр
 2class IFilter {
 3public:
 4    virtual float process(float sample) = 0;
 5    virtual ~IFilter() = default;
 6};
 7
 8// Конкретные стратегии
 9class LowPassFilter : public IFilter {
10    float prev_ = 0.0f;
11    float alpha_;
12public:
13    explicit LowPassFilter(float alpha) : alpha_(alpha) {}
14    float process(float sample) override {
15        prev_ = alpha_ * sample + (1.0f - alpha_) * prev_;
16        return prev_;
17    }
18};
19
20class MedianFilter : public IFilter {
21    std::array<float, 5> window_{};
22    size_t pos_ = 0;
23public:
24    float process(float sample) override {
25        window_[pos_++ % window_.size()] = sample;
26        auto sorted = window_;
27        std::sort(sorted.begin(), sorted.end());
28        return sorted[sorted.size() / 2];
29    }
30};
31
32// Контекст — использует любую стратегию, которую хранит
33class SensorPipeline {
34    IFilter* filter_;
35public:
36    explicit SensorPipeline(IFilter* filter) : filter_(filter) {}
37
38    void setFilter(IFilter* filter) { filter_ = filter; }
39
40    float read(float raw) {
41        return filter_->process(raw);
42    }
43};

Использование:

1LowPassFilter lp(0.1f);
2MedianFilter  med;
3
4SensorPipeline pipeline(&lp);
5pipeline.read(adcSample());
6
7// Смена стратегии во время выполнения
8pipeline.setFilter(&med);
9pipeline.read(adcSample());

Когда использовать: стратегия должна выбираться во время выполнения (конфиг пользователя, согласование протокола, тип сенсора не известен при компиляции).

Стоимость: один виртуальный вызов на каждый process. На Cortex-M4 на 168 МГц косвенный вызов через vtable стоит ~3–5 нс — незначительно, если только не вызывается в плотном внутреннем цикле на мегасэмплах.


Стратегия времени компиляции — параметр шаблона

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

 1template <typename Filter>
 2class SensorPipeline {
 3    Filter filter_;
 4public:
 5    float read(float raw) {
 6        return filter_.process(raw);
 7    }
 8};
 9
10// Использование
11SensorPipeline<LowPassFilter> pipeline;    // LowPassFilter встроен
12SensorPipeline<MedianFilter>  pipeline2;   // другой двоичный, другая оптимизация

Классы фильтров больше не нуждаются в наследовании от IFilter — утиная типизация через шаблоны. process просто должен существовать с правильной сигнатурой.

 1// Наследование не нужно для шаблонной стратегии
 2class KalmanFilter {
 3    // ... состояние
 4public:
 5    float process(float sample) {
 6        // Обновление фильтра Калмана
 7        return estimate_;
 8    }
 9};
10
11SensorPipeline<KalmanFilter> kpipeline;

Когда использовать: конкретная стратегия известна при компиляции. Распространено во встраиваемых системах, где один алгоритм выбирается для всей сборки прошивки и никогда не меняется во время выполнения. Шаблонные стратегии позволяют полное встраивание и автовекторизацию.

Стоимость: размер кода — каждый SensorPipeline<X> — отдельный двоичный. На маленьких МК с 64 кБ flash инстанциация 5 стратегий фильтра с SensorPipeline производит 5 отдельных копий класса контекста.


Проектирование на основе политик — несколько ортогональных стратегий

Контекст часто имеет больше одной оси вариации. Шаблонные политики позволяют компоновать их независимо:

 1template <typename Filter, typename Logger, typename Output>
 2class SensorPipeline {
 3    Filter  filter_;
 4    Logger  logger_;
 5    Output  output_;
 6public:
 7    void run(float raw) {
 8        float filtered = filter_.process(raw);
 9        logger_.log(filtered);
10        output_.send(filtered);
11    }
12};
13
14// Комбинируйте на своё усмотрение
15using ProductionPipeline = SensorPipeline<KalmanFilter, NullLogger, UartOutput>;
16using DebugPipeline      = SensorPipeline<LowPassFilter, UartLogger, LcdOutput>;
17
18ProductionPipeline prod;
19DebugPipeline      debug;

NullLogger — пустая структура с методом log, ничего не делающим — компилятор полностью устраняет её. Это «нулевой объект» в форме типа: нулевая стоимость в производстве, полное логирование в отладке, выбирается при компиляции.


CRTP-стратегия — статический полиморфизм с синтаксисом наследования

CRTP предоставляет виртуальный синтаксис с нулевыми накладными расходами:

 1// CRTP-база — интерфейс через static_cast к производному
 2template <typename Derived>
 3class FilterBase {
 4public:
 5    float process(float sample) {
 6        return static_cast<Derived*>(this)->processImpl(sample);
 7    }
 8};
 9
10class LowPassFilter : public FilterBase<LowPassFilter> {
11    float prev_ = 0.0f;
12    float alpha_;
13public:
14    explicit LowPassFilter(float alpha) : alpha_(alpha) {}
15    float processImpl(float sample) {
16        prev_ = alpha_ * sample + (1.0f - alpha_) * prev_;
17        return prev_;
18    }
19};
20
21// Шаблонный контекст с CRTP-базой — нет виртуального вызова
22template <typename F>
23class SensorPipeline {
24    F filter_;
25public:
26    float read(float raw) { return filter_.process(raw); }
27};

Вызов process разрешается в processImpl через статический cast — полностью встраивается во время компиляции. Полезно, когда нужна иерархия наследования (для документации и ясности API) без накладных расходов.


Выбор правильного подхода

Критерий Виртуальный в рантайме Параметр шаблона CRTP
Стратегия выбирается во время выполнения Да Нет Нет
Нулевые накладные расходы Нет (vtable) Да (встроен) Да (static cast)
Хранить разные стратегии в контейнере Да (вектор IFilter*) Нет Нет
Размер кода на инстанциацию Общий N копий N копий
Типобезопасность Динамическая (ошибка в рантайме) Статическая (ошибка при компиляции) Статическая
Иерархия наследования Да Нет Да

Правило большого пальца:

  • Один алгоритм, выбранный при запуске: параметр шаблона
  • Алгоритм переключается во время выполнения: виртуальный вызов
  • Базовый класс фреймворка с переопределениями в производных: CRTP (если важна производительность) или виртуальный (если важна ясность)

Устранение switch — паттерн реестра

Стратегии времени выполнения часто требуют фабрики. Избегайте switch, строя реестр:

 1#include <functional>
 2#include <map>
 3#include <string>
 4#include <memory>
 5
 6using FilterFactory = std::function<std::unique_ptr<IFilter>()>;
 7
 8class FilterRegistry {
 9    std::map<std::string, FilterFactory> factories_;
10public:
11    void registerFilter(std::string name, FilterFactory f) {
12        factories_[std::move(name)] = std::move(f);
13    }
14
15    std::unique_ptr<IFilter> create(const std::string& name) const {
16        auto it = factories_.find(name);
17        if (it == factories_.end()) return nullptr;
18        return it->second();
19    }
20};
21
22// Регистрация (при запуске или через статический инициализатор)
23FilterRegistry registry;
24registry.registerFilter("lowpass", [] { return std::make_unique<LowPassFilter>(0.1f); });
25registry.registerFilter("median",  [] { return std::make_unique<MedianFilter>(); });
26
27// Без switch — открыт для новых стратегий без изменения кода реестра
28auto filter = registry.create(configValue);

Добавление новой стратегии — написать новый класс и один вызов регистрации — существующий код не трогается.


Итоги

  • Стратегия в рантайме: виртуальная база + конкретные классы + контекст с указателем — переключение во время выполнения, один вызов через vtable
  • Шаблонная стратегия: параметр типа на контексте — нулевая стоимость, встроена, должна быть известна при компиляции
  • На основе политик: несколько ортогональных параметров шаблона — компоновка стратегий независимо, нулевые стратегии бесплатны
  • CRTP-стратегия: статический вызов с синтаксисом наследования — разрешение при компиляции, иерархия наследования без vtable
  • Реестр: словарь фабричных функций — добавление стратегий без правки существующего кода

Что дальше