Паттерн «Фабрика» на современном C++

Добавлено 21 февраля 2022 в 23:31

Паттерн «Фабрика» – еще один распространенный паттерн, который нам нужно знать. Главный принцип фабрики заключается в том, что она отделяет создание от использования. Но что это значит? Мы разрабатываем объект фабрику, который создает нужные нам объекты в зависимости от того, что мы хотим. Непосредственно в рабочем коде мы не используем конструкторы. Мы обращаемся к фабрике, заказываем тип объекта, который хотим получить, и фабрика возвращает этот объект. Непонятно? Тогда рассмотрим пример с использованием современного 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;
}
UML диаграмма классов неоптимизированной версии
UML диаграмма классов неоптимизированной версии

Что не так сейчас? Хм, во-первых, тот факт, что у 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;
}
UML диаграмма классов оптимизированной версии
UML диаграмма классов оптимизированной версии

Наша функция 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;
}

Теги

C++ / CppПаттерны проектирования / Design PatternsПрограммированиеФабрика / Factory

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.