Exploring Design Patterns – Abstract Factory

Let's look at how can maintain a factory of factories which is called as an Abstract Factory.

While developing an application, using a Factory pattern helps in solving problems related to the choice of object type selection and instantiation in large scale applications, where the decision of choosing a specific concrete implementation for a specific type is taken during runtime.

While this works for a single set of concrete implementations which are related over a common subject, consider the scenario wherein this single set of concrete components are replicated over several dimensions causing in a multiple levels of related components which have a common theme.

In such cases we are left with not one but several factories each of which intend to serve the decision of choosing over a set of component options. Now the client or the end user should not be aware of this, but instead be provided an abstraction using which he gets his job done.

Then arises the need for another layer of abstraction over these set of factories which has the choice to pick one factory for a given scenario. This is what we call an Abstract Factory pattern.

What is an Abstract Factory?

An Abstract Factory pattern is one of the twenty three design patterns defined to solve a specific problem of object oriented design, and this solves the particular problem of choosing a particular implementation over multiple levels of sets of components which share a common theme. It can be simply put as a “Factory of Factories”.

We create a layer of abstraction over the factories and the client creates a concrete implementation of the abstract factory interface and then uses the object to access the concrete object. In this case the client doesn’t know which concrete object he has received from the factory.

“An Abstract Factory can be thought as a Factory of Factories”

For example, lets take the scenario of a vehicle manufacturer who has three different variants of a motorcycle he designed: quarter litre, litre and a commuter caliber models. If a customer has to look for the vehicle’s details, the customer is provided a ProductFactory which can provide a specific concrete implementation for his choice (such as a quarter-litre, litre or a commute). Let’s assume there are three variants of any motorcycle: say Generic, Retro and Limited Edition models.

wp-content/uploads/2022/05/abstract_factory.png

When we combine both, we end up in three variants of motorcycle which come in three calibrations each – a total of 9 distinctive motorcycles. But a customer is not interested in all these details to know the details of a particular model. He just gives in a variant and a caliber values to the system to get the details for his requirement. For this, we make use of an abstract factory of products which further branches out into three factories for each variant. Each factory results in a concrete implementation of a single specific caliber model.

Implementing Abstract Factory – Example

Basing on the product example stated above let’s look at how we can convert the words into classes and implement an abstract factory for the above scenario. All it begins it at a type Product for which we have families of variants relying on. Let’s create an abstract type IProduct with a single method ShowProductInfo() that displays the product information for the variant chosen.

namespace ReadersFactoryApp.Providers
{
    public interface IProduct
    {
        void ShowProductInfo();
    }
}

And there exist three variants of these products based on the engine caliber:

namespace ReadersFactoryApp.Providers
{
    public interface IQuarterLitreProduct : IProduct { }
    public interface ILitreProduct : IProduct { }
    public interface IEconomyProduct : IProduct { }
}

And these have their own implementations as follows:

namespace ReadersFactoryApp.Providers
{

    public class EconomyProduct : IEconomyProduct
    {
        string type;

        public EconomyProduct(string type)
        {
            this.type = type;
        }

        public void ShowProductInfo()
        {
            Console.WriteLine(
 
quot;This is a {type} Product of Economy caliber"); } } public class QuarterLitreProduct : IQuarterLitreProduct { string type; public QuarterLitreProduct(string type) { this.type = type; } public void ShowProductInfo() { Console.WriteLine(
 
quot;This is a {type} Product of QuarterLitre caliber"); } } public class LitreProduct : ILitreProduct { string type; public LitreProduct(string type) { this.type = type; } public void ShowProductInfo() { Console.WriteLine(
 
quot;This is a {type} Product of Litre caliber"); } } } 

We can observe that each of these concrete implementations receive a type parameter through the constructor which conveys the variant of the Product it is: Generic, LimitedEdition or Retro.

And there exists an abstract base type IRootProductFactory which forms the base for all the factories representing the variants stated above.

namespace ReadersFactoryApp.Providers
{
    public interface IProductFactory
    {
        IProduct CreateProduct();
    }

    public interface IRetroProductFactory : IProductFactory {}

    public interface ILimitedEditionProductFactory : IProductFactory {}

    public interface IGenericProductFactory : IProductFactory {}
}

And the concrete implementations for each of these factories decide which of the product to return.

namespace ReadersFactoryApp.Providers
{
    // A Generic Factory which returns
    // the instances for each Product
    public class Factory
    {
        public static IProduct CreateProduct(string type)
        {
            switch (type.ToLowerInvariant())
            {
                case "quarter":
                    return new QuarterLitreProduct(type);
                case "litre":
                    return new LitreProduct(type);
                case "economy":
                default:
                    return new EconomyProduct(type);
            }
        }

        // A static Factory which returns
        // an instance for each Factory
        public static IProductFactory CreateFactory(string model, string type)
        {
            IProductFactory factory;

            switch (model.ToLowerInvariant())
            {
                case "limitededition":
                    factory = new LimitedEditionProductFactory(type);
                    break;
                case "retro":
                    factory = new RetroProductFactory(type);
                    break;
                case "generic":
                default:
                    factory = new GenericProductFactory(type);
                    break;
            }

            return factory;
        }
    }

    public class GenericProductFactory : IGenericProductFactory
    {
        string type;
        public GenericProductFactory(string type)
        {
            this.type = type;
        }
        public IProduct CreateProduct()
        {
            return Factory.CreateProduct(type);
        }
    }

    public class LimitedEditionProductFactory : ILimitedEditionProductFactory
    {
        string type;
        public LimitedEditionProductFactory(string type)
        {
            this.type = type;
        }
        public IProduct CreateProduct()
        {
            return Factory.CreateProduct(type);
        }
    }

    public class RetroProductFactory : IRetroProductFactory
    {
        string type;
        public RetroProductFactory(string type)
        {
            this.type = type;
        }
        public IProduct CreateProduct()
        {
            return Factory.CreateProduct(type);
        }
    }
}

Now we have three families of factory types along with three product types which depend on them for choice. Now we can’t simply ask the client to chose for himself the type needed. Instead we call upon a RootProductFactory that does the job for us. The IRootProductFactory is the abstract type that is used by the client for the invocation.

namespace ReadersFactoryApp.Providers
{
    public interface IRootProductFactory
    {
        IProductFactory CreateFactory(string model, string type);
    }

    public class RootProductFactory : IRootProductFactory
    {
        public RootProductFactory()
        {
        }

        public IProductFactory CreateFactory(string model, string type)
        {
            return Factory.CreateFactory(model, type);
        }
    }
}

The AbstractFactory type RootProductFactory forms a base for the families of factories here (Limited / Retro / Generic factories) and on the Client side there’s only one abstract type and a method CreateFactory() to call which returns the desired product. The choice is made by passing the model and type of product required as parameters to the factory method.

The AbstractFactory interface and implementation can then be added as a service into the ASP.NET Core container to be able to inject anywhere.

The Client code can be assumed as below:

namespace ReadersFactoryApp.Providers
{
    public class Client
    {
        IRootProductFactory factory;

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

        public void StartingPoint(string model, string type)
        {
            IProduct product = factory.CreateFactory(model, type).CreateProduct();
            product.ShowProductInfo();
        }
    }
}

In this way, we can implement an abstract factory pattern at its simplest form. As mentioned earlier, one must not try to introduce an abstract factory pattern or any pattern for that sake into the application from the very beginning which might end up in messing up the code; but instead try to adapt only when it is needed.

Advantages:

  • The intention of the pattern is simple: to insulate the creation of objects from their usage and create families of related types without having to depend on their concrete implementations.
  • This results in better management of the types, and since we have an abstract layer of factory over the families of factories; we can easy interchange the concrete implementations without even having to change the code that accesses these objects.
  • The client can never know what change has happened in the background since all he looks at is a single plain abstract base factory type for invocation.

Things to Keep in Mind:

  • But on the con side, we end up creating a huge chain of factories and implementations which end up in a complex network of abstract and concrete types.
  • These are hard to main and even harder to implement.

Like as all other design patterns, We shouldn’t try to implement these from the very beginning and instead look for a pattern to apply only when needed.

Sriram Mannava
Sriram Mannava

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

Leave a Reply

Your email address will not be published. Required fields are marked *