Abstract Factory Design 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).

  • This 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 product: LightText and LightBgcolor for the light theme and DarkText and DarkBgcolor for the dark theme.

Java implementation: 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
}

Java implementation: 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 on the basis of 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. In order 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. So the code is hard to maintain!
  • 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 Single Responsibility Principle.

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

Better solution idea using Abstract Factory

We can solve the problem of hard-code instantiation of products using interfaces. For this, we declare interfaces for each product (BgColor and Text), and implement the concrete subclasses for specific themes (LightText, DarkText, LightBgColor, DarkBgColor).

We also create an abstract ThemeFactory class that declares an interface consisting of methods to instantiate each product family. Now, for each product family, we create concrete factories (LightFactory and DarkFactory) derived from the ThemeFactory.

The clients will call methods of the ThemeFactory to obtain product instances, but at the same time, they are unaware of the concrete classes they're using. So, clients remain independent of the current family of products.

The last step is to decide which variant of the product family will be used (in the main()). Accordingly, an object of that factory is passed to the client code, which then uses abstract product classes to create variant-specific concrete products!

Abstract factory pattern solution example

Implementation steps

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

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! But we ensure interaction between products of the same family only for the sake of uniformity and flow in the application. 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 as it is taken care of by the Client code.
  • 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 and introduce it in the code without touching the Client class.

Components and Structure

Abstract factory pattern UML components and structure

  • Abstract Products: Interfaces for each product.
  • Concrete Products: Concrete sub-classes of appropriate 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: It takes an abstract factory object and instantiates corresponding product families. It only interacts via the interfaces and not through concrete classes.

When to apply Abstract Factory?

We should use the Abstract Factory when:

  • We need to use families of related products, and we want to avoid dependency on the concrete classes of those products. The 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.
  • System should be easily extensible in terms of new families, without coupling the code to concrete classes.

Advantages of Abstract Factory

  • It 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).
  • It 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.
  • This pattern makes changing product families very easy.

Applications of Abstract Factory

Some of the popular uses of this design pattern can be found in:

  • 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.

However, it is worth mentioning that adding new products can become challenging when following this design pattern due to the hierarchy of interfaces and concrete classes. Nonetheless, the Abstract Factory pattern offers several advantages in terms of code organization and flexibility.

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!

Share Your Insights

☆ 16-week live DSA course
☆ 16-week live ML course
☆ 10-week live DSA course

More from EnjoyAlgorithms

Self-paced Courses and Blogs

Coding Interview

Machine Learning

System Design

Our Newsletter

Subscribe to get well designed content on data structure and algorithms, machine learning, system design, object orientd programming and math.