Паттерны

Обзор паттернов проектирования: зачем нужны, GoF классификация, антипаттерны

Введение

Цикл синтаксиса C/C++ разобрал ООП-инструменты языка — наследование, виртуальные функции, шаблоны — но не systematized, как именно эти инструменты комбинируются для решения типичных, повторяющихся архитектурных задач. Паттерны проектирования — устоявшиеся, многократно проверенные решения именно таких типичных задач, и этот новый цикл систематически разберёт классическую классификацию GoF (Gang of Four — авторов основополагающей книги по теме) через призму современного C++ и Qt.

Концепция

Паттерн проектирования — не конкретный, готовый к копированию код, а скорее общая СХЕМА решения определённой категории задачи, адаптируемая к конкретному контексту — GoF делит 23 классических паттерна на три категории: порождающие (creational), структурные (structural — как организовывать отношения между классами и объектами), и поведенческие (behavioral — как объекты взаимодействуют и распределяют ответственность). Важно понимать паттерны не как обязательный, универсальный набор «правильных» решений, применимых всегда, а как словарь — общий язык для описания архитектурных решений, и знание паттернов ценно прежде всего для УЗНАВАНИЯ уже знакомой структуры в новом коде и для эффективной коммуникации с другими разработчиками («здесь нужен Observer»), а не для механического, не обязательного применения каждого паттерна к каждой задаче.

Пример кода

// Антипаттерн — "Золотой молоток" (Golden Hammer): применение знакомого паттерна
// там, где простое, прямое решение было бы лучше
class OverEngineeredCalculator
{
public:
    class AdditionStrategy { public: virtual int calculate(int a, int b) { return a + b; } virtual ~AdditionStrategy() = default; };
    // Создание целой иерархии Strategy для единственной, простой, неизменной операции —
    // ИЗБЫТОЧНАЯ сложность без реальной потребности в подстановке разных алгоритмов
    std::unique_ptr<AdditionStrategy> m_strategy = std::make_unique<AdditionStrategy>();
    int add(int a, int b) { return m_strategy->calculate(a, b); }
};

// Простое, ПРЯМОЕ решение — без необходимости в паттерне там, где он не оправдан
int add(int a, int b) { return a + b; }
// Узнавание паттерна, уже реализованного средствами самого языка/фреймворка —
// Qt сигналы/слоты являются ГОТОВОЙ реализацией паттерна Observer 
#include <QObject>

class TemperatureSensor : public QObject
{
    Q_OBJECT
signals:
    void temperatureChanged(double newValue); // Observer "из коробки" — без необходимости писать вручную
};

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

OverEngineeredCalculator показывает классический антипаттерн — применение полной инфраструктуры паттерна Strategy для задачи, у которой нет и не предвидится реальной потребности в подстановке альтернативных алгоритмов, что добавляет сложность (дополнительные классы, виртуальные вызовы) без какой-либо практической выгоды по сравнению с простой, обычной функцией add(int, int). Пример с Qt-сигналами показывает обратную, не менее важную мысль — многие паттерны (особенно Observer) уже реализованы готовыми механизмами современных фреймворков, и понимание паттерна Observer концептуально помогает осознать, ЧТО именно сигналы/слоты Qt решают архитектурно, не требуя написания этого механизма вручную с нуля каждый раз.

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

  • Механическое применение паттернов «потому что это считается хорошей практикой» без анализа, решает ли конкретный паттерн реальную, присутствующую в задаче проблему — как демонстрирует антипаттерн «Золотой молоток», избыточное применение паттернов добавляет сложность кода (дополнительные классы, уровни индирекции) без соразмерной практической выгоды, и осознанное решение «паттерн X решает реально присутствующую здесь проблему Y» должно предшествовать его применению, а не следовать механически из самого факта знания паттерна.
  • Незнание, что многие паттерны уже реализованы готовыми механизмами используемого языка/фреймворка (Observer через сигналы/слоты Qt, Iterator через диапазонные циклы C++11) — попытка реализовать «паттерн с нуля» там, где язык или фреймворк уже предоставляет готовое, идиоматичное решение той же задачи, неоправданно усложняет код по сравнению с использованием уже доступного механизма.
  • Изучение паттернов как набора жёстких, неизменных «рецептов» вместо понимания решаемой каждым паттерном проблемы — реальное применение паттерна в конкретном проекте почти всегда требует адаптации к специфике задачи (другие имена, иная конкретная структура классов), и буквальное, неизменное копирование «канонической» структуры паттерна из учебника без понимания, ПОЧЕМУ эта структура решает конкретную проблему, часто приводит к неуместному, формальному применению паттерна там, где он не подходит content-wise.
  • Игнорирование стоимости введения паттерна для понимания кода другими разработчиками, особенно менее опытными или незнакомыми с конкретным паттерном — паттерны дают ценность как общий, разделяемый язык именно тогда, когда команда действительно владеет этим языком, и введение редкого, малоизвестного или излишне «экзотического» паттерна в командный проект без предварительного обсуждения может скорее усложнить, чем облегчить понимание кода для остальных участников команды.