Abstract Factory (A Creational Design Pattern in OOPS)

Abstract Factory is a creational design pattern that helps in providing an interface for producing 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” or “features”.

Let’s understand via problem

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

  • This editor has two key features: bgcolour and text. Therefore, a family of products consists of bgcolour and text. We also want to provide two themes for the editor: light and dark.
  • So we have two families of products i.e., light theme-based bgcolour + text and dark theme-based bgcolour + text.

A basic solution approach

We declare a base class for each feature, like BgColor for the bgcolour feature and Text for the text feature. Then we declare subclasses for each theme-based product. For e.g., LightText and LightBgcolour for light theme products and DarkText and DarkBgcolour for dark theme products.

C++ implementation: BgColor base class and concrete products

class BgColor 
{
    //Operations for bgcolour
};

class LightBgcolour : public BgColor 
{
    //Operations for light-theme bgcolor
};

class DarkBgcolour : public BgColor 
{
    //Operations for dark-theme bgcolor
};

C++ implementation: Text base class and concrete products

class Text
{
    //Operations for text
};

class LightText : public Text
{
    //Operations for light-theme text
};

class DarkText : public Text
{
    //Operations for dark-theme text
};

Now we define code for the instantiation of theme-based concrete 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.

class Client 
{
    private:
    BgColour *bgcolor;
    Text *text;
    
    public:
    //parameterized constructor
    Client(string theme) 
    {
        if(theme == "Light") 
        {
            bgcolour = new LightBgcolour;
            text = new LightText;
        }
        else 
        {
            bgcolour = new DarkBgcolour;
            text = new DarkText; 
        }
    }
    
    //Operations using bgcolour and text
};

int main() 
{
    string input_theme; 
    cout << "Enter the theme- Light or Dark : \n";
    cin >> input_theme;
    Client client(input_theme);
}

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. A good code should have minimal coupling between different modules. To know more about coupling and related practices, check out this blog.
  • 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 too, which leads to the breaking of 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 as it is responsible for instantiating all types of products. Additionally, this class might have operations on these products too which again violates SRP.

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

A better solution idea using Abstract Factory

Solution Idea

We can solve this problem of hard-code instantiation and the creation of products using interfaces for each product family and each product feature. For this, we declare interfaces for each product feature of the (i.e., bgcolour and text), and the concrete subclasses implement product features for specific themes (lighttext, lightbgcolour, etc.).

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

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

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

Solution Steps

  • We create abstract product classes  BgColor and Text. Then, we implement methods of these abstract products in each theme-based concrete products class, e.g., LightBgcolour, DarkBgcolour, LightText, and DarkText.
  • 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.
  • 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 (the Client code) doesn’t get coupled to a specific variant of products.
  • Then we add client code that will work with factories and products via their interfaces.
  • Further, based on the current theme, the suitable object of the factory is passed to the client code. The entire 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.

Solution Code C++

Abstract product class for bgcolour

class BGCOLOUR 
{
    public:
    virtual string set_bg() = 0;
};

Concrete products of bgcolour for each variant using BGCOLOUR

class light_bgcolour : public BGCOLOUR 
{
    public:
    string set_bg() override 
    {
        return "light.";
    }
};

class dark_bgcolour : public BGCOLOUR 
{
    public:
    string set_bg() override 
    {
        return "dark.";
    }
};

Note: All products should be able to interact with each other! But we must ensure interaction between products of the same family only for the sake of uniformity and flow in the application.

Abstract product class for text consists of an additional method for interaction between bgcolour and text products.

class TEXT 
{
    public:
    virtual string set_txt() = 0;
    virtual string interact(BGCOLOUR &interactor) = 0;
    //By using Abstract factory, we make sure that both products are of //the same family
};

concrete products of text for each variant using TEXT

class light_text : public TEXT 
{
    public:
    string set_txt() override 
    {
        return "text - light";
    }
    
    string interact(BGCOLOUR &interactor) override 
    {
        string result = interactor.set_bg();
        return ("text-light interacted with "+ result);
    }
};


class dark_text : public TEXT 
{
    public:
    string set_txt() override 
    {
        return "text-dark";
    }
  
    string interact(BGCOLOUR &interactor) override 
    {
        string result = interactor.set_bg();
        return ("text-dark interacted with "+ result);
    }
};

Abstract Factory, provides methods to create each distinct product

class ThemeFactory 
{
    public:
    virtual BGCOLOUR *create_bg() = 0;
    virtual TEXT *create_txt() = 0;
};

Concrete factories, derived from Abstract factory implement methods of Abstract factory to produce appropriate products (Concrete factories for light and dark theme).

class LightFactory : public ThemeFactory 
{
    public:
    BGCOLOUR *create_bg() override 
    {
        return new light_bgcolour();
    }
    
    TEXT *create_txt() override 
    {
        return new light_text();
    }
};

class DarkFactory : public ThemeFactory 
{
    public:
    BGCOLOUR *create_bg() override 
    {
        return new dark_bgcolour();
    }
    
    TEXT *create_txt() override 
    {
        return new dark_text();
    }
};

The client uses only the abstract classes ThemeFactory, BGCOLOUR, and TEXT to instantiate proper products.

void Client(ThemeFactory &factory) 
{
    BGCOLOUR *bgcolour = factory.create_bg();
    TEXT *text = factory.create_txt();
    cout << bgcolour->set_bg() << " " << text->set_txt() << "\n";
    cout << text->interact(*bgcolour) << "\n";
}

int main() 
{
    string input_theme; 
    cout << "Enter the theme- Light or Dark : \n";
    cin >> input_theme;
    ThemeFactory *in;
    
    if (input_theme == "Light")
        in = new LightFactory;
    else if (input_theme == "Dark")
        in = new DarkFactory;
    else 
    {
        cout << "ERROR\n";
        exit(0};
    } 
    //pasing current theme to Client for instantiation
    Client (*in);
    //changing factory type of "in", we can change the family
    //without going into concrete classes
}

Solution Code Java

import java.util.*;

public class java_abstract 
{
    //product interface for bgcolour    
    public static interface BGCOLOUR 
    {
        String set_bg();
    }
    
    //concrete products
    public static class light_bgcolour implements BGCOLOUR 
    {
        @Override
        public String set_bg() 
        {
            return ("light.");
        }
    }

    public static class dark_bgcolour implements BGCOLOUR 
    {
        @Override
        public String set_bg() 
        {
            return ("dark.");
        }
    }

    //product interface for text 
    public static interface TEXT 
    {
        String set_txt();
        String interact(BGCOLOUR interactor);
    }
    
    //concrete products
    public static class light_txt implements TEXT 
    {
        @Override
        public String set_txt() 
        {
            return ("text-light");
        }
    
        @Override
        public String interact (BGCOLOUR interactor) 
        {
            String res = interactor.set_bg();
            return ("text-light interacted with "+ res);
        }
    }

    public static class dark_txt implements TEXT 
    {
        @Override
        public String set_txt() 
        {
            return("text-dark");
        }
        
        @Override
        public String interact (BGCOLOUR interactor) 
        {
            String res = interactor.set_bg();
            return ("text-dark interacted with "+ res);
        }
    }
    
    //Abstract Factory - ThemeFactory
    public static interface ThemeFactory 
    {
        BGCOLOUR create_bg();
        TEXT create_txt();
    }
    
    //concrete factories
    public static class LightFactory implements ThemeFactory 
    {
        @Override
        public BGCOLOUR create_bg() 
        {
            return new light_bgcolour();
        }
        
        @Override
        public TEXT create_txt() 
        {
            return new light_txt();
        }
    }
    
    public static class DarkFactory implements ThemeFactory 
    {
        @Override
        public BGCOLOUR create_bg() 
        {
            return new dark_bgcolour();
        }
        
        @Override
        public TEXT create_txt() 
        {
            return new dark_txt();
        }
    }

    //Client code for instantiation, makes sure all products are compatible
    public static void Client (ThemeFactory factory)
    {
        BGCOLOUR bgcolour = factory.create_bg();
        TEXT text = factory.create_txt();
        
        System.out.println (bgcolour.set_bg() + " " + text.set_txt());
        System.out.println ('\n' + text.interact(bgcolour)+'\n');
    }
    
    //main call
    public static void main(String[] args) 
    {
        Scanner sc= new Scanner(System.in); 
        System.out.println("Enter the theme- Light or Dark : \n");  
        String input_theme = sc.nextLine();
        ThemeFactory in;
        
        if(input_theme.equals("Light"))
        {
            in = new LightFactory();
            //instantiation light theme
            Client(in);
        }
        else if (input_theme.equals("Dark"))
        {
            in = new DarkFactory();
            //instantiaon dark theme
            Client(in);
        }
        else
        {
            System.out.println ("ERROR! \n");
            System.exit(0);
        }
    }
}

Key Takeaways

Using this approach, we can observe the following:

  • 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.
  • The Client class is not coupled to the concrete products because the Client class is written to the interfaces i.e., it deals with the abstract classes and the interfaces only. Therefore, it is easy to maintain or update concrete classes.
  • The 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

In the UML diagram:

  • Solid arrows depict entities that the Client uses.
  • Dashed lines depict inheritance.
  • The solid line for family1 and the dotted line for family2.
  • Abstract Products (humidity, radiance)
    Provide interfaces for each specific product of the general family.
  • Concrete Products (highhumidity, lowhumidity, highradiance, lowradiance)
    Sub-classes of appropriate abstract products provide implementation details of variant-specific (family-specific) products.
  • Abstract Factory (SetFactory)
    An interface that declares methods for the creation of each abstract product.
  • Concrete Factories (HighSetFactory, LowSetFactory)
    Sub-classes of abstract factory implement the creation methods of all the family-specific products via their concrete classes.
  • Client (Client)
    It takes an abstract factory object and hence instantiates corresponding product families. It only interacts via the interfaces and not through concrete classes.

NOTE: Although the methods of concrete factories instantiate concrete products, the return type of each method is set as the corresponding abstract product.

When to apply the idea of Abstract Factory?

We should use the Abstract Factory pattern when

  • The type of products to be created is not known a priori.
  • A system should be independent of how its products are created, maintained, and represented.
  • A system is constrained to use a particular family of related products among various such families, and the user should not be bothered about it.
  • A system should instantiate its products via interfaces only, and their implementations are not revealed.
  • A 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. (Lacked in 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, i.e., maintains consistency among products.
  • As already seen, this pattern makes changing product families very easy.

Disadvantages of Abstract Factory

  • Many interfaces and their sub-classes make the code complex and large.
  • Adding new products is difficult. Because the Abstract Factory fixes the products. To add new products, we have to change Abstract Factory and all the factories it follows.

Applications of Abstract Factory

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

  • In many Java libraries, e.g., in XPathFactory, the newInstance() method returns a factory object of XPathFactory, used to create XPath objects.
  • Another such Java library: TransformerFactory. (Working is analogous to XPathFactory)
  • ET++ (object-oriented application framework implemented in C++) uses this design pattern for portability across different window systems.
  • InterViews (C++ graphical interface toolkit) denotes AbstractFactory classes with a “Kit” suffix such as DialogKit and WidgetKit.

Conclusion

Abstract Factory pattern is a creational design pattern that should be used when various families of more than one product; all the instantiated products should belong to a single-family. The responsibility of correct instantiation of product is not the user’s concern. Moreover, it is in line with the Open-Closed, Single responsibility principles. We also saw the issues caused by the naive hard-coded family-based instantiation of various products and solved them using the Abstract Factory pattern. Then, we saw the scenarios where it is applicable, e.g., when a system should be independent of how the products are created, when a system is prone to add new families in the future, etc. Also, adding new products will be difficult if we follow this design pattern because of the hierarchy of the interfaces and concrete class. Then we saw some uses of this design pattern!

Enjoy learning, Enjoy design patterns!

More From EnjoyAlgorithms

© 2022 Code Algorithms Pvt. Ltd.

All rights reserved.