Зачем централизовать создание объектов

Когда подсистема создаёт свои зависимости напрямую, она связана с их конкретными типами. Изменение реализации означает изменение кода подсистемы:

1// Задача сенсора создаёт собственные зависимости — привязана к Bmp280 и UartLogger
2class SensorTask {
3    Bmp280     sensor_;   // конкретный тип
4    UartLogger logger_;   // конкретный тип
5public:
6    SensorTask() : sensor_(hi2c1), logger_(huart2) {}
7};

Тестирование в изоляции требует реального железа. Замена Bmp280 на мок требует правки SensorTask. Паттерн «фабрика» выносит решение о создании за пределы класса, использующего объект.


Фабричный метод

Фабричный метод — функция, которая создаёт и возвращает объект, скрывая конкретный тип за интерфейсом:

 1// Интерфейс
 2class ISensor {
 3public:
 4    virtual float read() = 0;
 5    virtual ~ISensor() = default;
 6};
 7
 8// Конкретные сенсоры
 9class Bmp280Sensor : public ISensor { /* ... */ };
10class MockSensor   : public ISensor {
11    float value_;
12public:
13    explicit MockSensor(float v) : value_(v) {}
14    float read() override { return value_; }
15};
16
17// Фабричная функция — вызывающий не знает, какой конкретный тип получает
18std::unique_ptr<ISensor> createSensor(SensorType type) {
19    switch (type) {
20        case SensorType::Bmp280: return std::make_unique<Bmp280Sensor>(hi2c1);
21        case SensorType::Mock:   return std::make_unique<MockSensor>(25.0f);
22        default:                 return nullptr;
23    }
24}
25
26auto sensor = createSensor(SensorType::Bmp280);
27float temp = sensor->read();

Switch живёт в одном месте. SensorTask получает ISensor* — и никогда не видит Bmp280Sensor.

Статический фабричный метод на классе

Для создания единственного типа с несколькими путями создания поместите фабрику на сам класс:

 1class FrameParser {
 2public:
 3    static FrameParser fromUart(UART_HandleTypeDef* huart) {
 4        return FrameParser(huart, FrameFormat::Uart);
 5    }
 6    static FrameParser fromSpi(SPI_HandleTypeDef* hspi) {
 7        return FrameParser(hspi, FrameFormat::Spi);
 8    }
 9
10private:
11    FrameParser(void* handle, FrameFormat fmt) : handle_(handle), format_(fmt) {}
12    void*       handle_;
13    FrameFormat format_;
14};
15
16auto parser = FrameParser::fromUart(&huart1);

Именованные конструкторы (fromUart, fromSpi) передают намерение лучше, чем одни только списки параметров — особенно когда несколько перегрузок принимают одинаковые типы.


Абстрактная фабрика

Абстрактная фабрика группирует связанные фабрики за одним интерфейсом. Вызывающий выбирает семейство фабрики; фабрика создаёт согласованные объекты внутри этого семейства:

 1// Интерфейс абстрактной фабрики
 2class IPeripheralFactory {
 3public:
 4    virtual std::unique_ptr<ISensor>  createSensor()  = 0;
 5    virtual std::unique_ptr<IDisplay> createDisplay() = 0;
 6    virtual std::unique_ptr<IStorage> createStorage() = 0;
 7    virtual ~IPeripheralFactory() = default;
 8};
 9
10// Фабрика реального железа
11class Stm32Factory : public IPeripheralFactory {
12public:
13    std::unique_ptr<ISensor>  createSensor()  override { return std::make_unique<Bmp280Sensor>(hi2c1); }
14    std::unique_ptr<IDisplay> createDisplay() override { return std::make_unique<Ssd1306Display>(hi2c1); }
15    std::unique_ptr<IStorage> createStorage() override { return std::make_unique<FlashStorage>(); }
16};
17
18// Тестовая фабрика — все моки, никакого железа
19class MockFactory : public IPeripheralFactory {
20public:
21    std::unique_ptr<ISensor>  createSensor()  override { return std::make_unique<MockSensor>(23.5f); }
22    std::unique_ptr<IDisplay> createDisplay() override { return std::make_unique<NullDisplay>(); }
23    std::unique_ptr<IStorage> createStorage() override { return std::make_unique<RamStorage>(4096); }
24};
25
26// Приложение — принимает любую фабрику
27class Application {
28    std::unique_ptr<ISensor>  sensor_;
29    std::unique_ptr<IDisplay> display_;
30    std::unique_ptr<IStorage> storage_;
31public:
32    explicit Application(IPeripheralFactory& factory)
33        : sensor_ (factory.createSensor())
34        , display_(factory.createDisplay())
35        , storage_(factory.createStorage())
36    {}
37};
38
39// main.cpp — выбираем фабрику один раз
40#ifdef UNIT_TEST
41MockFactory factory;
42#else
43Stm32Factory factory;
44#endif
45Application app(factory);

Переключение с железа на моки — изменение одной строки. Всё подключение моков находится в MockFactory, а не разбросано по Application.


Паттерн «строитель»

Паттерн «строитель» создаёт сложные объекты шаг за шагом. Полезен, когда:

  • Создание объекта требует многих необязательных параметров
  • Параметры имеют сложные ограничения валидации или порядка
  • Нужен текучий интерфейс (цепочка методов)

Текучий строитель

 1class UartConfig {
 2public:
 3    uint32_t baud     = 115200;
 4    uint8_t  wordLen  = 8;
 5    uint8_t  stopBits = 1;
 6    bool     parity   = false;
 7    bool     flowCtrl = false;
 8};
 9
10class UartBuilder {
11    UartConfig cfg_;
12public:
13    UartBuilder& baud(uint32_t b)       { cfg_.baud = b;       return *this; }
14    UartBuilder& wordLength(uint8_t w)  { cfg_.wordLen = w;    return *this; }
15    UartBuilder& stopBits(uint8_t s)    { cfg_.stopBits = s;   return *this; }
16    UartBuilder& parityEven()           { cfg_.parity = true;  return *this; }
17    UartBuilder& hardwareFlowControl()  { cfg_.flowCtrl = true;return *this; }
18
19    UartConfig build() const { return cfg_; }
20};
21
22// Использование — читабельно, нет путаницы с позиционными аргументами
23auto cfg = UartBuilder{}
24    .baud(9600)
25    .wordLength(8)
26    .stopBits(2)
27    .parityEven()
28    .build();

Сравните с альтернативой через конструктор:

1UartConfig cfg(9600, 8, 2, true, false);  // что означает каждый bool?

Строитель делает каждый аргумент самодокументируемым.

Строитель с валидацией

 1class UartBuilder {
 2    UartConfig cfg_;
 3    bool baudrateSet_ = false;
 4
 5public:
 6    UartBuilder& baud(uint32_t b) {
 7        if (b != 9600 && b != 115200 && b != 921600)
 8            throw std::invalid_argument("неподдерживаемая скорость");
 9        cfg_.baud = b;
10        baudrateSet_ = true;
11        return *this;
12    }
13
14    UartConfig build() const {
15        if (!baudrateSet_)
16            throw std::logic_error("скорость не задана");
17        return cfg_;
18    }
19};

Валидация при сборке ловит неправильно настроенные объекты до того, как они доберутся до железа.

Строитель для тестовых фикстур

Строители особенно полезны в тестах, где нужно много похожих объектов с небольшими вариациями:

 1class SensorBuilder {
 2    float    baseTemp_   = 20.0f;
 3    float    noiseLevel_ = 0.0f;
 4    bool     faulted_    = false;
 5public:
 6    SensorBuilder& temperature(float t) { baseTemp_ = t;    return *this; }
 7    SensorBuilder& noise(float n)       { noiseLevel_ = n;  return *this; }
 8    SensorBuilder& faulted()            { faulted_ = true;  return *this; }
 9
10    std::unique_ptr<ISensor> build() const {
11        if (faulted_) return std::make_unique<FaultingSensor>();
12        return std::make_unique<MockSensor>(baseTemp_, noiseLevel_);
13    }
14};
15
16// Тестовые случаи
17auto coldSensor    = SensorBuilder{}.temperature(-10.0f).build();
18auto noisySensor   = SensorBuilder{}.temperature(25.0f).noise(2.0f).build();
19auto brokenSensor  = SensorBuilder{}.faulted().build();

Каждый тестовый случай читается чётко без повторяющихся списков аргументов конструктора.


Реестр фабрик

Для расширяемости в стиле плагинов регистрируйте фабрики в словаре:

 1class SensorRegistry {
 2    using Factory = std::function<std::unique_ptr<ISensor>()>;
 3    std::map<std::string, Factory> factories_;
 4
 5public:
 6    void registerSensor(std::string name, Factory f) {
 7        factories_[std::move(name)] = std::move(f);
 8    }
 9
10    std::unique_ptr<ISensor> create(const std::string& name) const {
11        auto it = factories_.find(name);
12        if (it == factories_.end()) return nullptr;
13        return it->second();
14    }
15};
16
17SensorRegistry registry;
18registry.registerSensor("bmp280",  [] { return std::make_unique<Bmp280Sensor>(hi2c1); });
19registry.registerSensor("ntc10k",  [] { return std::make_unique<NtcSensor>(hadc1); });
20registry.registerSensor("mock",    [] { return std::make_unique<MockSensor>(22.0f); });
21
22// Создание сенсора из конфигурации
23auto sensor = registry.create(config.get("sensor_type"));

Новые типы сенсоров добавляются регистрацией — никаких изменений в switch, никаких правок существующего кода. Это принцип открытости/закрытости, применённый к созданию объектов.


Итоги

  • Фабричный метод: одна функция создаёт одно семейство объектов — вызывающий видит интерфейс, а не тип
  • Именованный конструктор: статическая фабрика на классе — передаёт намерение для нескольких путей создания
  • Абстрактная фабрика: интерфейс для создания семейств связанных объектов — замена всех реализаций (железо → мок) за раз
  • Строитель: пошаговое создание с цепочкой методов — читаемая конфигурация, валидация при сборке
  • Реестр фабрик: словарь фабричных функций — расширение в стиле плагинов без switch

Что дальше