Паттерн «Фабрика» на современном C++
Паттерн «Фабрика» – еще один распространенный паттерн, который нам нужно знать. Главный принцип фабрики заключается в том, что она отделяет создание от использования. Но что это значит? Мы разрабатываем объект фабрику, который создает нужные нам объекты в зависимости от того, что мы хотим. Непосредственно в рабочем коде мы не используем конструкторы. Мы обращаемся к фабрике, заказываем тип объекта, который хотим получить, и фабрика возвращает этот объект. Непонятно? Тогда рассмотрим пример с использованием современного C++?
Внимание, спойлер: это не подробный технический текст. Это просто пример, который пытается непрофессиональным языком объяснить сухую тему. Если вы очень серьезно относитесь к этому, то сначала попробуйте почитать соответствующую книгу.
Что будет нашим примером? Это вымышленный экспортер аудио и видео. Мы не реализуем его полностью, а попытаемся построить каркас. В начале мы покажем неоптимизированный пример, а затем, как мы его улучшим с помощью паттерна фабрика. Исходный вариант определяет абстрактные классы (интерфейсы) для экспортеров видео и аудио.
struct AudioExporter
{
virtual void PrepareExport(std::string_view data) = 0;
virtual void DoExport(const std::filesystem::path &folder) = 0;
virtual ~AudioExporter() = default;
};
а также
struct VideoExporter
{
virtual void PrepareExport(std::string_view data) = 0;
virtual void DoExport(const std::filesystem::path &folder) = 0;
virtual ~VideoExporter() = default;
};
Мы просим каждый подкласс реализовать функции PrepareExport
(подготавливает медиафайл к экспорту) и DoExport
(выполняет экспорт по нужному пути). Затем мы реализуем наши подклассы для видео для следующих форматов:
- экспортер видео без потерь;
- экспортер видео H.264 (Baseline);
- экспортер видео H.264 (Hi).
Всё вместе в одном файле выглядит так:
struct LossLessVideoExporter final : public VideoExporter
{
void PrepareExport(std::string_view data) override
{
spdlog::info("Preparing video data for lossless export.");
}
void DoExport(const std::filesystem::path &folder) override
{
spdlog::info("Exporting video data in lossless format to {}.", folder.c_str());
}
};
struct H264BPVideoExporter final : public VideoExporter
{
void PrepareExport(std::string_view data) override
{
spdlog::info("Preparing video data for H.264 (Baseline) export.");
}
void DoExport(const std::filesystem::path &folder) override
{
spdlog::info("Exporting video data in H.264 (Baseline) format to {}.", folder.c_str());
}
};
struct H264Hi422PVideoExporter final : public VideoExporter
{
void PrepareExport(std::string_view data) override
{
spdlog::info("Preparing video data for H.264 (Hi422P) export.");
}
void DoExport(const std::filesystem::path &folder) override
{
spdlog::info("Exporting video data in H.264 (Hi422P) format to {}.", folder.c_str());
}
};
И аналогично для аудио мы реализуем следующие форматы
- экспортер аудио AAC;
- экспортер аудио WAV (без потерь).
struct AACAudioExporter final : public AudioExporter
{
void PrepareExport(std::string_view data) override
{
spdlog::info("Preparing audio data for AAC export.");
}
void DoExport(const std::filesystem::path &folder) override
{
spdlog::info("Exporting audio data in AAC format to {}.", folder.c_str());
}
};
struct WAVAudioExporter final : public AudioExporter
{
void PrepareExport(std::string_view data) override
{
spdlog::info("Preparing audio data for WAV export.");
}
void DoExport(const std::filesystem::path &folder) override
{
spdlog::info("Exporting audio data WAV format to {}.", folder.c_str());
}
};
Затем в нашей функции main
, которая является примером использования, мы просим пользователя ввести желаемое качество (low (низкое) или hi (высокое)), а затем в зависимости от ввода создаем объекты соответствующих классов и выполняем тестовый экспорт. В этом примере вы можете видеть, что обеспечения гибкости при объявлении экспортеров мы используем полиморфизм. Но, как известно, код никогда не врёт. Давайте посмотрим на неоптимизированную версию кода.
int main()
{
std::shared_ptr<AudioExporter> audio_exporter;
std::shared_ptr<VideoExporter> video_exporter;
std::cout << "Enter desired output quality (low, high, master): ";
std::string export_quality;
std::cin >> export_quality;
if (export_quality == "low")
{
audio_exporter = std::make_shared<AACAudioExporter>(AACAudioExporter{});
video_exporter = std::make_shared<H264BPVideoExporter>(H264BPVideoExporter{});
}
else
{
audio_exporter = std::make_shared<WAVAudioExporter>(WAVAudioExporter{});
video_exporter = std::make_shared<LossLessVideoExporter>(LossLessVideoExporter{});
}
audio_exporter->PrepareExport("placeholder_for_audio_data");
video_exporter->PrepareExport("placeholder_for_video_data");
std::filesystem::path media_path {"/usr/media/"};
audio_exporter->DoExport(media_path);
video_exporter->DoExport(media_path);
return 0;
}
Что не так сейчас? Хм, во-первых, тот факт, что у main
слишком много обязанностей. Она спрашивает пользователя, чего он хочет, создает средства экспорта, подготавливает медиафайлы и выполняет экспорт (низкая связность). Функция main
должна знать о существовании всех классов и напрямую зависит от них. Кажется, это не совсем элегантно. Что, если мы используем что-то, что где-то создает объекты, которые нам нужны, и возвращает эти объекты нам (в main
). Мы (функция main
) хотим использовать их, не задействуя так много деталей. Да, нашей функции main
не нужно знать, как создавать объекты; она запрашивает и получает их. Теперь вы видите, с этим подходом нам удастся свести связь к минимуму. Даже если мы добавим в нашу фабрику больше вариантов различных объектов, функция main
останется прежней.
Но как мы реализуем паттерн фабрика?
Во-первых, нам нужно создать класс ExporterFactory
. Мы используем этот абстрактный класс (интерфейс), чтобы запрашивать экспортеров видео и аудио. Всё просто:
struct ExporterFactory
{
virtual std::unique_ptr<VideoExporter> GetVideoExporter() = 0;
virtual std::unique_ptr<AudioExporter> GetAudioExporter() = 0;
virtual ~ExporterFactory() = default;
};
Как видите, фабрика возвращает уникальные указатели. Да, фабрике не принадлежат объекты, которые она создает.
Затем нужно создать конкретные фабрики, которые реализуют интерфейс ExporterFactory
. Для примера мы произвольно выбрали фабрики для следующих ситуаций:
- быстрый экспортер (высокая скорость, более низкое качество)
struct FastExporter final : public ExporterFactory { std::unique_ptr<VideoExporter> GetVideoExporter() override { return std::make_unique<H264BPVideoExporter>(H264BPVideoExporter {}); } std::unique_ptr<AudioExporter> GetAudioExporter() override { return std::make_unique<AACAudioExporter>(AACAudioExporter {}); } };
- экспортер с высоким качеством (медленная скорость, высокое качество)
struct HighQualityExporter final : public ExporterFactory { std::unique_ptr<VideoExporter> GetVideoExporter() override { return std::make_unique<H264Hi422PVideoExporter>(H264Hi422PVideoExporter {}); } std::unique_ptr<AudioExporter> GetAudioExporter() override { return std::make_unique<AACAudioExporter>(AACAudioExporter {}); } };
- экспортер с исходным качеством (низкая скорость, исходное качество)
struct MasterQualityExporter final : public ExporterFactory { std::unique_ptr<VideoExporter> GetVideoExporter() override { return std::make_unique<LossLessVideoExporter>(LossLessVideoExporter {}); } std::unique_ptr<AudioExporter> GetAudioExporter() override { return std::make_unique<WAVAudioExporter>(WAVAudioExporter {}); } };
Переходим к функции main
и создаем функцию ReadExporter
, которая свяжет нас с фабрикой:
std::unique_ptr<ExporterFactory> ReadExporter()
{
std::map<std::string, std::unique_ptr<ExporterFactory>> factories;
factories["low"] = std::make_unique<FastExporter>(FastExporter {});
factories["high"] = std::make_unique<HighQualityExporter>(HighQualityExporter {});
factories["master"] = std::make_unique<MasterQualityExporter>(MasterQualityExporter {});
while (true)
{
std::cout << "Enter desired output quality (low, high, master): ";
std::string export_quality;
std::cin >> export_quality;
if (factories.contains(export_quality))
{
return std::move(factories[export_quality]);
}
std::cout << "There is something wrong, please try again." << std::endl;
}
}
int main()
{
auto exporter = ReadExporter();
auto video_exporter = exporter->GetVideoExporter();
auto audio_exporter = exporter->GetAudioExporter();
audio_exporter->PrepareExport("placeholder_for_audio_data");
video_exporter->PrepareExport("placeholder_for_video_data");
std::filesystem::path media_path {"/usr/media/"};
audio_exporter->DoExport(media_path);
video_exporter->DoExport(media_path);
return 0;
}
Наша функция ReadExporter
избавляет код main
от запроса пользователя об его предпочтениях и создания нужных экспортеров. Функция main
вызывает функцию ReadExporter
, которая возвращает уникальный указатель на интерфейс ExporterFactory
. Функция ReadExporter
возвращает один из конкретных объектов фабрик, которые мы разработали ранее. Внутри функции ReadExporter
мы используем контейнер STL map
, чтобы связать ответы пользователей (текст) с конструкторами фабрик экспортеров. Мы используем наследование, умные указатели и чувствуем себя счастливыми. Жизнь хороша тем, что эти ребята отвечают за обеспечение совместимости между интерфейсами и подклассами. Если вы посмотрите сейчас на main
, вы увидите нечто изысканное. Мы просим функцию ReadExporter
вернуть экспортер, из которого мы берем соответствующие экспортеры аудио и видео.
Для поклонников шаблонов мы могли бы пойти еще дальше и использовать внедрение зависимостей в новом методе, использующем экспортеры. Так что да, у нас есть разделение на создание и использование (как я и обещал в начале), и теперь main
наслаждается тишиной, имея всего две инструкции. Неплохо?
std::unique_ptr<ExporterFactory> ReadExporter()
{
std::map<std::string, std::unique_ptr<ExporterFactory>> factories;
factories["low"] = std::make_unique<FastExporter>(FastExporter {});
factories["high"] = std::make_unique<HighQualityExporter>(HighQualityExporter {});
factories["master"] = std::make_unique<MasterQualityExporter>(MasterQualityExporter {});
while (true)
{
std::cout << "Enter desired output quality (low, high, master): ";
std::string export_quality;
std::cin >> export_quality;
if (factories.contains(export_quality))
{
return std::move(factories[export_quality]);
}
std::cout << "There is something wrong, please try again." << std::endl;
}
}
void Exporter(std::unique_ptr<ExporterFactory> exporter)
{
auto video_exporter = exporter->GetVideoExporter();
auto audio_exporter = exporter->GetAudioExporter();
audio_exporter->PrepareExport("placeholder_for_audio_data");
video_exporter->PrepareExport("placeholder_for_video_data");
std::filesystem::path media_path {"/usr/media/"};
audio_exporter->DoExport(media_path);
video_exporter->DoExport(media_path);
}
int main()
{
auto exporter = ReadExporter();
Exporter(std::move(exporter));
return 0;
}