Creational patterns – Abstract Factory

Overview

In this post we will see how and when to use the Abstract Factory design pattern. If you want to get an overview of design patterns please check the introduction series ( PART1, PART2 and PART3 ).

Creational Patterns abstract the instantiation process therefore they:

  • encapsulate knowledge about concrete classes
  • hides how instances of classes are created and put together

Intent

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Note: ideas in this post are extracted from Design Patterns: Elements of Reusable Object-Oriented Software.

Motivation

  • We have a cross platform user interface library, witch provides widgets can be used on Windows and Linux.
  • The standards for creating user interfaces differ on each system.
  • An application should not hard code its widgets for a particular platform.

In main method we have an ifdef for each widget instantiation depending on the platform.

int main()
{
  
  std::unique_ptr<Button> button;
#ifdef  WIN64
  button = std::make_unique<ButtonWindows>();
#else
  button = std::make_unique<ButtonPosix>();
#endif //  WIN64

  std::unique_ptr<Rectangle> rectangle;

#ifdef  WIN64
  rectangle = std::make_unique<RectangleWindows>();
#else
  rectangle = std::make_unique<RectanglePosix>();
#endif //  WIN64

  std::unique_ptr<TextBox> textBox;
#ifdef  WIN64
  textBox = std::make_unique<TextBoxWindows>();
#else
  textBox = std::make_unique<TextBoxPosix>();
#endif //  WIN64

  button->press();
  rectangle->draw();
  textBox->getInput();

  return 0;
}

Problems

  • If we want to add a new OS we will have to insert another ifdef and so on.
  • For every new instance we will have to add and ifdef for each platform.
  • Let’s try to imagine what would happen if we have a huge code base and we have to introduce 2 new platforms (not something I would like to do).

Code

Now lets see how the code looks for the textbox implementation. You can find the complete code here.

TextBox class represents the interface that has to be implemented by each platform specific textbox.

class TextBox
{
public:
  virtual void getInput() = 0;
};

TexBoxPosix class represents the POSIX (Linux) implementation.

class TextBoxPosix : public TextBox

{
public:
  virtual void getInput()
  {
  std::cout << "POSIX TextBox input received" << std::endl;
  }
};

TextBoxWindows class represents the Windows implementation.

class TextBoxWindows: public TextBox
{
public:
  virtual void getInput()
  {
    std::cout << "Windows TextBox input received" << std::endl;
  }
};

We have similar implementations for button and rectangle widgets.

Class diagram

Now lets make the class UML diagram for our current situation.

Initial class diagram

As we see the Client in our case the main method instantiates every instance of a widget, so it depends on every implementation of the widgets.

In a more formal way we could say that when we create an object we specify the class explicitly. We depend on specific instances instead of depending on abstractions (an interface).

Can we solve this and the problems specified above? Lets see how would the class diagram of the implementation looks.

Implementation

Abstract Factory pattern  applied
Abstract Factory pattern applied

WidgetFactory interface provides the means to create each widget type. The client can use this interface to receive concrete widgets for each platform without knowing about their implementation.

WidgetFactoryPosix class provides the means to instantiate widgets for Linux platforms.

WidgetFactoryWindows class provides the means to instantiate widgets for Windows platforms.

Now lets identify the pattern components.

Pattern structure

Abstract Factory  pattern structure
Abstract Factory structure

Pattern participants :

  • AbstractFactory : WidgetFactory
    • declares an interface that creates abstract products
  • ConcreteFactory: WidgetFactoryPosix , WidgetFactoryWindows
    • implements the operations to create concrete objects ( an implementation of AbstractFactory)
  • AbstractProduct: Button, TextBox and Rectangle
    • declares an interface for a product type
  • Product: ButtonLinux, ButtonWindows, RectangleWindows etc.
    • implements the AbstractProduct interface
    • defines an object that will be created by the concrete factory
  • Client: uses only interfaces declared by AbstractFactory and AbstractProduct

Consequences

PROS:

  • Isolates concrete classes, encapsulating the responsibility of creating objects. You uses product and factory interfaces.
  • The concrete implementations can be easily changed.

CONS:

  • Adding a new product classes is difficult.

Implementation

The full code can be found here.

Note: The TexBox, Button and Rectangle interfaces and their implementations remain unchanged.

The WidgetFactory interface provides the a create operation for each widget type.

class WidgetFactory
{
public:
  virtual std::unique_ptr<Button> createButton() = 0;

  virtual std::unique_ptr<Rectangle> createRectangle() = 0;
  
  virtual std::unique_ptr<TextBox> createTexBox() = 0;
};

The WidgetFactoryPosix is the realization of WidgetFactory interface and returns the POSIX version instances of the widgets.

class WidgetFactoryPosix : public WidgetFactory
{
public:
  virtual std::unique_ptr<Button> createButton();

  virtual std::unique_ptr<Rectangle> createRectangle();

  virtual std::unique_ptr<TextBox> createTexBox();
};

std::unique_ptr<Button> WidgetFactoryPosix::createButton()
{
  return std::make_unique<ButtonPosix>();
}

std::unique_ptr<Rectangle> WidgetFactoryPosix::createRectangle()
{
  return std::make_unique<RectanglePosix>();
}

std::unique_ptr<TextBox> WidgetFactoryPosix::createTexBox()
{
  return std::make_unique<TextBoxPosix>();
}

Consequences

Adding a new platform

What happens when we want to create widgets for our newly supported Fuchsia OS.

  • Add a new class WidgetFactoryFuchsia OS that implements WidgetFactory.
  • Add implementations for each widget type.

The above changes are OK because we extend our code.

Adding a new widget type

Suppose we want to add a ScrollBar widget.

  • Add a new method to the WidgetFactory to create the specified widget
  • Add implement the method in all of widget factory realizations

The above changes are not OK because we modify our existing code.

Conclusions

  • Isolates concrete classes – we request the WidgetFactory to create the instances and use the widget interfaces to manipulate the.
  • Exchanging products families easy – you only have to change the factory implementation when you change the product family.
  • Adding new products is not easy – see adding a scroll bar.

When not to use it:

  • When the overhead is to big (example you will need only one ifdef in your code).
  • When you have only one type of products (you library is only for windows) – maybe you could use a factory method.
  • etc.

You can find the introduction into design patterns series here: PART1, PART2 and PART3. In the next post will explore the Builder pattern.

If you see things that you want to be improved write a comment or contact me at blog@adriannecula.eu. Thanks, Adrian.

Leave a Comment