Зачем централизовать создание объектов
Когда подсистема создаёт свои зависимости напрямую, она связана с их конкретными типами. Изменение реализации означает изменение кода подсистемы:
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
Что дальше
- Внедрение зависимостей без фреймворка — передача фабрик через конструкторы
- Проектирование на основе интерфейсов — интерфейсы, которые возвращают фабрики
- Паттерн «стратегия» — фабрика + стратегия для алгоритмов, выбираемых в рантайме