Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Modern C++ Programming Cookbook

You're reading from   Modern C++ Programming Cookbook Master Modern C++ with comprehensive solutions for C++23 and all previous standards

Arrow left icon
Product type Paperback
Published in Feb 2024
Publisher Packt
ISBN-13 9781835080542
Length 816 pages
Edition 3rd Edition
Languages
Arrow right icon
Author (1):
Arrow left icon
Marius Bancila Marius Bancila
Author Profile Icon Marius Bancila
Marius Bancila
Arrow right icon
View More author details
Toc

Table of Contents (15) Chapters Close

Preface 1. Learning Modern Core Language Features 2. Working with Numbers and Strings FREE CHAPTER 3. Exploring Functions 4. Preprocessing and Compilation 5. Standard Library Containers, Algorithms, and Iterators 6. General-Purpose Utilities 7. Working with Files and Streams 8. Leveraging Threading and Concurrency 9. Robustness and Performance 10. Implementing Patterns and Idioms 11. Exploring Testing Frameworks 12. C++ 20 Core Features 13. Other Books You May Enjoy
14. Index

Avoiding repetitive if-else statements in factory patterns

It is often the case that we end up writing repetitive if...else statements (or an equivalent switch statement) that do similar things, often with little variation and often done by copying and pasting with small changes. As the number of alternative conditions increases, the code becomes both hard to read and hard to maintain. Repetitive if...else statements can be replaced with various techniques, such as polymorphism. In this recipe, we will see how to avoid if...else statements in factory patterns (a factory is a function or object that is used to create other objects) using a map of functions.

Getting ready

In this recipe, we will consider the following problem: building a system that can handle image files in various formats, such as bitmap, PNG, JPG, and so on. Obviously, the details are beyond the scope of this recipe; the part we are concerned with is creating objects that handle various image formats. For this, we will consider the following hierarchy of classes:

class Image {};
class BitmapImage : public Image {};
class PngImage    : public Image {};
class JpgImage    : public Image {};

On the other hand, we’ll define an interface for a factory class that can create instances of the aforementioned classes, as well as a typical implementation using if...else statements:

struct IImageFactory
{
  virtual std::unique_ptr<Image> Create(std::string_view type) = 0;
};
struct ImageFactory : public IImageFactory
{
  std::unique_ptr<Image> 
  Create(std::string_view type) override
  {
    if (type == "bmp")
      return std::make_unique<BitmapImage>();
    else if (type == "png")
      return std::make_unique<PngImage>();
    else if (type == "jpg")
      return std::make_unique<JpgImage>();
    return nullptr;
  }
};

The goal of this recipe is to see how this implementation can be refactored to avoid repetitive if...else statements.

How to do it...

Perform the following steps to refactor the factory shown earlier to avoid using if...else statements:

  1. Implement the factory interface:
    struct ImageFactory : public IImageFactory
    {
      std::unique_ptr<Image> Create(std::string_view type) override
      { 
        // continued with 2. and 3.
      }
    };
    
  2. Define a map where the key is the type of objects to create and the value is a function that creates objects:
    static std::map<
      std::string,
      std::function<std::unique_ptr<Image>()>> mapping
    {
      { "bmp", []() {return std::make_unique<BitmapImage>(); } },
      { "png", []() {return std::make_unique<PngImage>(); } },
      { "jpg", []() {return std::make_unique<JpgImage>(); } }
    };
    
  3. To create an object, look up the object type in the map and, if it is found, use the associated function to create a new instance of the type:
    auto it = mapping.find(type.data());
    if (it != mapping.end())
      return it->second();
    return nullptr;
    

How it works...

The repetitive if...else statements in the first implementation are very similar – they check the value of the type parameter and create an instance of the appropriate Image class. If the argument to check was an integral type (for instance, an enumeration type), the sequence of if...else statements could have also been written in the form of a switch statement. That code can be used like this:

auto factory = ImageFactory{};
auto image = factory.Create("png");

Regardless of whether the implementation was using if...else statements or a switch, refactoring to avoid repetitive checks is relatively simple. In the refactored code, we used a map that has the key type std::string representing the type, that is, the name of the image format. The value is an std::function<std::unique_ptr<Image>()>. This is a wrapper for a function that takes no arguments and returns an std::unique_ptr<Image> (a unique_ptr of a derived class is implicitly converted to a unique_ptr of a base class).

Now that we have this map of functions that create objects, the actual implementation of the factory is much simpler; check the type of the object to be created in the map and, if present, use the associated value from the map as the actual function to create the object, or return nullptr if the object type is not present in the map.

This refactoring is transparent for the client code, as there are no changes in the way clients use the factory. On the other hand, this approach does require more memory to handle the static map, which, for some classes of applications, such as IoT, might be an important aspect. The example presented here is relatively simple because the purpose is to demonstrate the concept. In real-world code, it might be necessary to create objects differently, such as using a different number of arguments and different types of arguments. However, this is not specific to the refactored implementation, and the solution with the if...else/switch statement needs to account for that too. Therefore, in practice, the solution to this problem that worked with if...else statements should also work with the map.

There’s more...

In the preceding implementation, the map is a local static to the virtual function, but it can also be a member of the class or even a global. The following implementation has the map defined as a static member of the class. The objects are not created based on the format name, but on the type information, as returned by the typeid operator:

struct IImageFactoryByType
{
  virtual std::unique_ptr<Image> Create(
    std::type_info const & type)  = 0;
};
struct ImageFactoryByType : public IImageFactoryByType
{
  std::unique_ptr<Image> Create(std::type_info const & type) 
  override
  {
    auto it = mapping.find(&type);
    if (it != mapping.end())
      return it->second();
    return nullptr;
  }
private:
  static std::map<
    std::type_info const *,
    std::function<std::unique_ptr<Image>()>> mapping;
};
std::map<
  std::type_info const *,
  std::function<std::unique_ptr<Image>()>> ImageFactoryByType::mapping
{
  {&typeid(BitmapImage),[](){
      return std::make_unique<BitmapImage>();}},
  {&typeid(PngImage),   [](){
      return std::make_unique<PngImage>();}},
  {&typeid(JpgImage),   [](){
      return std::make_unique<JpgImage>();}}
};

In this case, the client code is slightly different, because instead of passing a name representing the type to create, such as PNG, we pass the value returned by the typeid operator, such as typeid(PngImage):

auto factory = ImageFactoryByType{};
auto movie = factory.Create(typeid(PngImage));

This alternative is arguably more robust because the map keys are not strings, which could be more prone to errors. This recipe proposes a pattern as the solution to a common problem, and not an actual implementation. As in the case of most patterns, there are different ways they can be implemented, and it is up to you to pick the one that is the most suitable for each context.

See also

  • Implementing the pimpl idiom, to learn a technique that enables the separation of the implementation details from an interface
  • Chapter 9, Using unique_ptr to uniquely own a memory resource, to learn about the std::unique_ptr class, which represents a smart pointer that owns and manages another object or array of objects allocated on the heap
lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image