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:
- Implement the factory interface:
struct ImageFactory : public IImageFactory { std::unique_ptr<Image> Create(std::string_view type) override { // continued with 2. and 3. } };
- 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>(); } } };
- 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