Factory pattern in modern C++

Factory pattern is another prevalent pattern that we need to know. The main principle about the factory is that it separates the creation from use. But what does this means? We design a factory entity that creates the objects we want depending on what we want. We do not use the constructors directly on the consuming side. We access the factory, we order the type of object that we want back, and the factory returns that object. Is it hard to understand? If yes, I can suggest two things. First, you can look at Arjan Codes related video, which explains the concept using Python. Then, why don’t you continue reading this tutorial which I’m going to present a similar example using modern C++?

Photo by Ant Rozetsky on Unsplash

Spoiler Alert: This is not a detailed technical text. It’s just an example that tries using layman’s terms to explain a dry topic. If you are very serious about that, please try a relevant technical book instead first.

What it’s going to be our example? It is a fictional audio and video exporter. We do not implement it in reality, but rather try to build the framework. In the beginning, we will show a non optimised example and then how we will improve it using the factory pattern. The initial script defines abstract classes (interfaces) for the video and the audio exporter.

struct AudioExporter {
  virtual void PrepareExport(std::string_view data) = 0;
  virtual void DoExport(const std::filesystem::path &folder) = 0;
  virtual ~AudioExporter() = default;

  using Ptr = std::shared_ptr<AudioExporter>;
};

and

struct VideoExporter {
  virtual void PrepareExport(std::string_view data) = 0;
  virtual void DoExport(const std::filesystem::path &folder) = 0;
  virtual ~VideoExporter() = default;
};

We request each subclass to implement the PrepareExport (prepares the media file to export) and the DoExport (perform the export to the desired path) functions. Then as we have learned so far, we implement our subclasses for both Video and Audio, as follows:

  • Lossless video exporter
  • H.264 video exporter (Baseline),
  • H.264 video exporter (Hi)

all together in one file like that:

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());
  }
};

and similarly, for the audio, we implement the following formats

  • AAC audio exporter and
  • WAV (lossless) audio exporter
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());
  }
};

Then in our main, which is an example of use, we request the user to enter the desired quality (low, high or master), and then depending on the input, we instantiate objects from the corresponding class and do a test export. In this example, you can see that we use polymorphism to allow flexibility in declaring the exports. But as you know, code never lies. Let’s have a look at the non optimised version of the code.

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;
}
The UML class diagram of the non-optimised version

What is wrong now? Hmm, first, it’s the fact that the main has too many responsibilities. It asks the user what they want, creates the exporters, prepares the media files, and perform the export (low cohesion).  As seen in other tutorials, together with that comes the coupling. The main function needs to know the existence of all classes and depends directly on them. It seems not elegant at all. What if we use something that creates the objects we want somewhere and returns the objects to us (in the main). We (the main method)  want to use them without involving so many details. Yep, our main does not need to know how to create the objects; it asks and gets served. Now you see, we manage to minimise the coupling with that approach. Even if we add more options of different objects in our factory, the main function remains the same.

I see your excitement, but how do we create a factory pattern?

First, we need to create the ExporteFactory class. We use this abstract class (interface) to ask for the video and the audio exporter. It is dead simple as that:

struct ExporterFactory {
  virtual std::unique_ptr<VideoExporter> GetVideoExporter() = 0;
  virtual std::unique_ptr<AudioExporter> GetAudioExporter() = 0;
  virtual ~ExporterFactory() = default;
};

As you see, the factory returns unique pointers. Yes, the factory does not own the objects it creates. No ownership.

Next, it comes to creating the concrete factories which implement the ExporterFactory interface. We arbitrary selected to develop factories for the following situations:

  • Fast exporter (high speed, lower quality)
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 {});
  }
};
  • High-Quality exporter (slower speed, high quality)
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 {});
  }
};
  • Master-Quality exporter (low speed, master quality)
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 {});
  }
};

We go to the main method and create the ReadExporter method, which will link us with the factory. Let’s have a look:

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;
}
The UML class diagram of the optimised version

Our ReadExporter method offloads the main method from asking the user about their preference and creating the desired exporters. The main method calls the ReadExporter method and expects the exporters for video and audio through the unique pointer to the ExporterFactory interface. The ReadExporter method returns one of the concrete exporter objects we designed earlier. Inside the ReadExporter method, we use a map STL container to relate users’ replies (texts) with exporters constructors. We use inheritance, smart pointers, and we feel happy. Life is good because these guys are responsible for ensuring compatibility between interfaces and subclasses. If you look now to the main, you will see something exquisite. Have a look! We ask the ReadExporter method to return the exporter from which we take the corresponding audio and video exporters.

For the patterns fans, we could go a step further and use dependency injection in a new method that uses the exporters. So yes, we have separation of the creation and use (as I promised at the beginning), and now main enjoys the silence by having only two commands. Sexy?

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;
}

 

The GitHub repository that has the complete example can be found in the following link

Special credits to the gents that I studied before I wrote this tutorial: