Abstract Factory Pattern

Abstract Factory is a creational design pattern that provides an interface for creating families of related objects (products) without specifying their concrete classes. In other words, it is a hierarchy of classes that encapsulates many possible “platforms” and the construction of a suite of “products”.

Problem statement

Suppose we are managing the visual appearance of an application (say a text editor).

  • The editor has two key features: bg color and text.
  • We also want to provide two themes for the editor: light and dark.
  • So we have two families of products: light theme-based bg color + text and dark theme-based bg color + text.

Basic solution approach

We declare an interface for each product: BgColor for the bg color and Text for the text. Then we declare subclasses for each family of products: LightText and LightBgcolor for the Light theme and DarkText and DarkBgcolor for the dark theme.

BgColor interface and concrete products

interface BgColor {
    // Operations for bgcolor
}

class LightBgColor implements BgColor {
    // Operations for light-theme bgcolor
}

class DarkBgColor implements BgColor {
    // Operations for dark-theme bgcolor
}

Text interface and concrete products

interface Text {
    // Operations for text
}

class LightText implements Text {
    // Operations for light-theme text
}

class DarkText implements Text {
    // Operations for dark-theme text
}

Now we define code for the instantiation of each family of products inside the Client class. To do this, we define a constructor inside the Client class, which is parameterized based on the theme. For the light theme, we instantiate light theme-based products, and likewise for the dark theme.

import java.util.Scanner;

class Client {
    private BgColor bgColor;
    private Text text;

    public Client(String theme) {
        if (theme.equals("Light")) {
            bgColor = new LightBgColor();
            text = new LightText();
        } 
        else {
            bgColor = new DarkBgColor();
            text = new DarkText();
        }
    }

    // Operations using bgColor and text
}

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("Enter the theme - Light or Dark: ");
        String inputTheme = scanner.nextLine();

        Client client = new Client(inputTheme);

        // Use the client object to perform operations using bgColor and text
    }
}

There are some drawbacks to this approach:

  • Client class is coupled to all the concrete product classes and is also hard to modify in the future. Suppose we want to add a new theme or remove an existing theme in the future. To do so, we have to alter the Client code, which will break the Open-Closed principle because the Client class is not closed for modification.
  • Client class does not follow the Single Responsibility Principle because it is responsible for instantiating all types of products. Additionally, this class might have operations on these products, which again violates the Single Responsibility Principle.

To overcome these drawbacks, we use an abstract factory design pattern.

Components and Structure of Abstract Factory

Abstract factory pattern UML components and structure

  • Abstract Products: Interfaces for each product.
  • Concrete Products: Concrete sub-classes of abstract products, which provide implementation details of each family-specific concrete product.
  • Abstract Factory: An interface that declares methods for the creation of each product.
  • Concrete Factories: Sub-classes of the abstract factory, which implement the creation methods of all the family-specific concrete products.
  • Client: Takes an abstract factory object and instantiates corresponding product families. It only interacts via the interfaces and not through concrete classes.

Solution idea using Abstract Factory

Abstract factory pattern solution example

  1. We create abstract product classes BgColor and Text. Then we implement methods of abstract products in each theme-based concrete product class (LightBgColor, DarkBgColor, LightText, and DarkText).
  2. We create the abstract factory ThemeFactory, which declares methods for creating all abstract products. Now for each theme, we create a concrete factory (DarkFactory and LightFactory) derived from the ThemeFactory interface.
  3. Each of these concrete factories returns products of a particular theme. Although these are concrete classes, the return type in the method signature should return abstract product types (BgColor and Text). By doing so, the part of code that uses factory objects (Client code) doesn’t get coupled to a specific variant of products.
  4. We add client code that will work with factories and products via their interfaces. For this, based on the current theme, we pass the suitable object of the factory to the client code. Here client code uses this factory object to create an appropriate family of products. Whenever the theme needs to be changed, we can pass the other factory object.

Implementation Code Java

Abstract and Concrete products of BgColor

abstract class BgColor {
    public abstract String setBg();
}

class LightBgColor extends BgColor {
    @Override
    public String setBg() {
        return "light.";
    }
}

class DarkBgColor extends BgColor {
    @Override
    public String setBg() {
        return "dark.";
    }
}

Note: All products should be able to interact with each other! For this, the abstract product class for Text will use a method for interaction between bg color and text products.

Abstract and concrete products of Text

abstract class Text {
    public abstract String setText();
    public abstract String interact(BgColor interactor);
}

class LightText extends Text {
    @Override
    public String setText() {
        return "text-light";
    }
  
    @Override
    public String interact(BgColor interactor) {
        String result = interactor.setBg();
        return "text-light interacted with " + result;
    }
}

class DarkText extends Text {
    @Override
    public String setText() {
        return "text-dark";
    }
  
    @Override
    public String interact(BgColor interactor) {
        String result = interactor.setBg();
        return "text-dark interacted with " + result;
    }
}

Abstract Factory (ThemeFactory) provides methods to create each distinct product and concrete factories (LightFactory and DarkFactory) implement these methods to produce appropriate products.

interface ThemeFactory {
    BgColor createBg();
    Text createText();
}

class LightFactory implements ThemeFactory {
    @Override
    public BgColor createBg() {
        return new LightBgColor();
    }

    @Override
    public Text createText() {
        return new LightText();
    }
}

class DarkFactory implements ThemeFactory {
    @Override
    public BgColor createBg() {
        return new DarkBgColor();
    }

    @Override
    public Text createText() {
        return new DarkText();
    }
}

The client uses only the ThemeFactory, BgColor, and Text to instantiate proper products.

class Client {
    private ThemeFactory factory;

    public Client(ThemeFactory factory) {
        this.factory = factory;
    }

    public void run() {
        BgColor bgColor = factory.createBg();
        Text text = factory.createText();
        System.out.println(bgColor.setBg() + " " + text.setText());
        System.out.println(text.interact(bgColor));
    }
}

class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter the theme - Light or Dark: ");
        String inputTheme = scanner.nextLine();
        ThemeFactory themeFactory;

        if (inputTheme.equals("Light"))
            themeFactory = new LightFactory();
        else if (inputTheme.equals("Dark"))
            themeFactory = new DarkFactory();
        else {
            System.out.println("ERROR");
            return;
        }

        Client client = new Client(themeFactory);
        client.run();
    }
}

Key Takeaways

  • Clients stay independent of the current variant, i.e., clients don’t need to worry about which variant of the product is to be instantiated. In other words, Client class is not coupled to the concrete products because the Client class deals with the abstract classes and the interfaces. So, it is easy to maintain or update concrete classes.
  • Client code is now following the Open-Closed principle. For adding a new family, we only need to declare a new factory that implements ThemeFactory.

When to apply Abstract Factory?

  • We need to use families of related products, and we want to avoid dependency on the concrete classes of those products. Here type of products to be created might be unknown beforehand, and the user should not be concerned about it.
  • The system should instantiate products only through interfaces, and the implementations should not be exposed. So our system should be independent of how products are created and maintained.
  • The system should be extensible in terms of new families, without coupling the code to concrete classes.

Advantages of Abstract Factory

  • Follows the Open-Closed principle as we can add another variant factory in the future without altering the existing code (whereas the conventional if-else-if method was lacking this).
  • Follows the Single Responsibility principle as we divide the responsibilities into different concrete classes and interfaces (lacking in the conventional if-else-if method).
  • Isolates the users from implementations of various products as users interact with interfaces only.
  • Uniformity of a single family is maintained throughout the program.
  • Makes changing product families very easy.

Applications of Abstract Factory

  • In many Java libraries. For example, in XPathFactory, the newInstance() method returns a factory object of XPathFactory, which is used to create XPath objects. Another notable Java library that utilizes this pattern is TransformerFactory (see here). Its functionality is similar to XPathFactory.
  • ET++ is an object-oriented application framework implemented in C++ that employs this design pattern for achieving portability across different window systems.
  • InterViews, a C++ graphical interface toolkit, uses the AbstractFactory classes with a "Kit" suffix, such as DialogKit and WidgetKit (see this paper).

Conclusion

Abstract Factory is a creational design pattern that should be used when there are multiple families of products and all the instantiated products should belong to a single family. The responsibility of correctly instantiating the products is not the user's concern.

We also observed the issues caused by the naive approach of hard-coding the instantiation of various products based on their families, and we solved them by using the Abstract Factory pattern.

Thanks to Ankit Nishad for his contribution in creating the first version of this content. If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, Enjoy oops!

More from EnjoyAlgorithms

Self-paced Courses and Blogs