Eric Ampire
Eric Ampire

Eric Ampire

The Abstract Factory Pattern explained

The Abstract Factory Pattern explained

Eric Ampire's photo
Eric Ampire

Published on Aug 18, 2021

7 min read

Subscribe to my newsletter and never miss my upcoming articles

Introduction

Design patterns are a common language that developers can use to communicate easily, in this post we will discover together the Abstract Factory design patterns using very practical examples that will allow you to better understand its usefulness and finally apply it in your everyday life.

Abstract Factory Pattern defined

The Abstract Factory Pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Problem

Let's imagine the creation of a GUI framework like Swing, your code contains the following classes:

A family of products belonging to the same theme depending on the operating system: Button, Textfield, Checkbox.

Several variants of this family. For example, the products Button, Textfield, Checkbox are available in the following variants: Mac, Window and Linux

You need to find a solution to create individual objects (graphical components) and match them to other objects of the same family. Customers get annoyed when they see a GUI with components that have nothing in common

Moreover, you don't want to rewrite your code every time you add new product families to your program. Developers regularly add new components to the framework

Solution

To solve these kinds of problems is where the Abstract Factory design pattern comes into play, it allows an interface that will define the contracts of all components that are supposed to be created

And it will be the responsibility of each implementation to choose which family of components will be created.

image.png

In the case of our GUI framework, we will use the Abstract Factory pattern to create our own different graphical component without worrying about the user or the operating system, the complete Abstract Factory adapted to our context looks like this.

example.png

If we look carefully, we can see that there are two graphical components, the button and the checkbox and each of these components has a family, in our case there are two families, the components of the Windows family and the components of the Mac family

If we look a little bit more carefully, we will realize that there are two interfaces, WinFactory and MacFactory, which implement the GUIFactory interface and which are responsible for the creation of the components of each family.

Implementation

All UI elements in a cross-platform application are expected to behave the same regardless of the operating system, but their appearance may vary slightly. You need to make sure that the UI elements match the style of the operating system. You don't want to end up with macOS buttons in Windows.

The abstract factory interface declares a set of creation methods that the client code can use to produce different types of UI elements. Each concrete factory corresponds to a particular operating system and creates its own UI elements based on that operating system.

The way it works is that when an application is executed, it checks the operating system used and uses this information to create an object of the factory that corresponds to it. This factory is then used to generate all the rest of the UI elements, thus avoiding errors.

Thanks to this approach, as long as it uses their abstract interfaces, the client code does not depend on the concrete classes of the factory and the elements of the UI. This also allows it to exploit new UI elements or factories that you may add in the future.

This way we don't have to modify the client code for every new UI element you want to add to your application. You will simply need to create a new factory class producing these elements and make some changes to the initialization code so that it chooses the appropriate class.

Defining Abstract Factory interface

For our Framework, we need to define different families of components, we need a button represented by the Button abstract class and all the subclass of this class will constitute the first family of components.

/**
 * This is the first family of object that we can create
 */
abstract class Button

class MacButton : Button()
class WindowsButton : Button()
class LinuxButton : Button()

We need a checkbox also represented by the CheckBox abstract class and all the sub classes of this class will constitute the second family of components of our framework.

/**
 * This is the second family of components that we can create
 */

abstract class CheckBox

class MacCheckBox : CheckBox()
class WindowsCheckBox : CheckBox()
class LinuxCheckBox : CheckBox()

As we have defined the components we will need to create in the future, let's now define our interface which just defines the contracts of the components that will be created, in our case we need to create a Button and a Checkbox, as shown in the following interface, the createButton and createCheckBox method returns an abstraction, and it will be the responsibility of the classes that will implement the GuiFactory interface to determine which concrete component will be created, it can be a WindowButton, MacButton etc...

The interface doesn't need to know, and that's why the Abstract Factory pattern is used to create loosely coupled code.

Note : Two components are said to be loosely coupled when these components interact without having much information about each other,

interface GuiFactory {

    /**
     * We can have one or more family of [Button]
     */
    fun createButton(): Button

    /**
     * We can have one or more family of [CheckBox]
     */
    fun createCheckBox(): CheckBox
}

Factory concrete

At this point we are ready to move on, we will now define the different implementations of GuiFactory interface that will be responsible for creating the components for each platform, Windows, Mac and Linux

Windows Factory

class WindowsFactory : GuiFactory {
    override fun createButton(): Button {
        return WindowsButton()
    }

    override fun createCheckBox(): CheckBox {
        return WindowsCheckBox()
    }
}

As with Windows, the MacFactory class follows the same logic but this time, it will be to create the components with the look and feel of MacOS

Mac Factory

class MacFactory : GuiFactory {
    override fun createButton(): Button {
        return MacButton()
    }

    override fun createCheckBox(): CheckBox {
        return MacCheckBox()
    }
}

Linux Factory

And to show how much the design pattern favors extensibility we have created another factory this time responsible for the creation of components for Linux, and we can create other factories depending on the number of platforms we want to support

class LinuxFactory : GuiFactory {
    override fun createButton(): Button {
        return LinuxButton()
    }

    override fun createCheckBox(): CheckBox {
        return LinuxCheckBox()
    }
}

The Final Pseudo Application

Now that all our components are ready, we are going to create a pseudo application that will illustrate the use of our different factory previously created.

The application just draws the graphical components independently of the platform, that's why the App class refers to the GuiFactory interface instead of referring to any implementation of this interface and by polymorphism, we can pass to the App class any factory as long as it implements the GuiFactory interface

And we won't have to modify our App application every time we need to support another platform (iOS or Android for example), isn't that great?

class App(val factory: GuiFactory) {

    fun render() {
        val button: Button = factory.createButton()
        val checkBox: CheckBox = factory.createCheckBox()
    }
}

The render method only has the information that it will handle a Button and a Checkbox without having to worry about the concrete implementation that will be used.

enum class Os {
    Windows, Linux, Mac
}

fun main() {
    val config = Os.Linux

    val app = when(config) {
        Os.Windows -> App(factory = WindowFactory())
        Os.Linux -> App(factory = LinuxFactory())
        Os.Mac -> App(factory = MacFactory())
    }
    app.render()
}

In the following code snippet, we create an object of type App by providing it with a factory according to the type of operating system on which the application will run, isn't it very convenient ?

Pros and Cons

Pros

  • You are assured that the products of a factory are compatible with each other.
  • You decouple the customer code from the actual products.
  • Single responsibility principle. You can move all product creation code to the same place, for better maintainability.
  • Open/closed principle. You can add new product variants without damaging the existing.

Cons

  • The code can become more complex than necessary, because this design pattern requires the addition of new classes and interfaces.

Conclusion

Use the abstract factory if your code needs to manipulate products of the same theme, but you don't want it to depend on the concrete classes of these products - either you don't know them yet, or you just want to make your code scalable.

The abstract factory provides an interface that allows you to create objects for each class in the product family. As long as your code uses this interface to create its objects, it will consistently take the correct variants of the products available in your application.

I hope this article has given you a clear idea of how the Design Pattern Abstract Factory works, feel free to contact me on Twitter if you have any question.

 
Share this