DEV Community

Cover image for Abstract Factory in C#
Kostas Kalafatis
Kostas Kalafatis

Posted on

Abstract Factory in C#

The Abstract Factory is a creational design pattern taht lets us produce families of related objects without specifying their concrete classes.

The Abstract Factory is a design pattern used to provide an interface for creating a group of factories with a common use case without specifying the concrete classes. Each concrete implementation of the abstract factory creates concrete objects that are part of the same family of products.

When using this pattern, we create factories which return different types of related objects. This pattern enables larger architectures such as Dependency Injection.

You can find the example code of this post, on GitHub

Conceptualizing the Problem

Imagine that we are creating a simulation application for a furniture-selling store. Our code consists of classes that either represent a family of products, such as Armchairs, Sofas and Cupboards, or variants of those families, such as Rustic, Urban Collective and Scandinavian Contemporary.

We need a way to create individual furniture objects so that tey match other objects of the same family. After all, customers can get quite upset if they receive non-matching furniture. Moreover, we don't want to change existing code when adding new products or families of products to the program.

The first thing the Abstract Factory pattern suggests, is to explicilty declare interfaces for each distinct product of the product family. Then we can make all variants of products follow the same interfaces. For example, all armchair variants can implement the Armchair interface; all sofa variants can implement the Sofa interface, and so on.

All variants of the same object must be moved to a single class hierarchy.

The next thing we are doing is to declare the Abstract Factory, an interface with a list of creation methods for all products that are part of the product family. The methods must return abstract product types represented by the interfaces we extracted previously: Armchair, Sofa, Cupboard, and so on.

Each concrete factory corresponds to a specific product variant.

Let's now consider the product variants. For each variant of a product family, we create a separate factory class based on the IFurnitureFactory interface. A factory is a class that returns products of a particular king. For example, the RusticFurnitureFactory can only create RusticArmchair, RusticSofa and RusticCupboard objects.

The client code has to work with both factories and products via their respective abstract interfaces. This lets us change the type of a factory that we pass to the client code, as well as the product variant that the client code receives, without breaking the actual client code.

Say the client wants a factory to produce a chair. The client doesn't have to be aware of the factory's class, nor does it matter what kind of chair it gets. Whether it's a Scandinavian Contemporary armchair or an Urban Collective armchair, the client must treat all chairs in the same manner, using the abstract IArmchair interface. With this approach, the client only knows that the product implements the SitOn method in some way. Also, whichever variant of the chair is returned, it'll always match the type of sofa or cupboard produced by the same factory object.

There's is a last thing to clarify: who creates the actual factory objects? Usually, the application creates a concrete factory object at the initialization stage. Just before that, the application must select the factory type depending on the configuration or the environment settings.

Structuring the Abstract Factory Pattern

In its base implementation, the Abstract Factory pattern has five participants:

Abstract Factory Class Diagram

  • Abstract Product: The Abstract Product declares interfaces for a set of distinct but related products which make up a product family.
  • Concrete Product: The Concrete Products are various implementations of abstract products, grouped by variants. Each abstract product (armchair/sofa) must be implemented in all given variants (Rustic/Scandinavian Contemporary).
  • Abstract Factory: The Abstract Factory interface declares a set of methods for creating each of the abstract products.
  • Concrete Factory: The Concrete Factories implement creation methods of the abstract factory. Each concrete factory corresponds to a specific variant of products and creates only those product variants.
  • Client: The Client can work with any concrete factory/product variant, as long as it communicates with their objects via abstract interfaces. Although concrete factories instantiate concrete products, signatures of their creation methods must return corresponding abstract products. This way the client code that uses a factory doesn't get coupled to the specific variant of the product it gets from a factory.

To demonstrate how the Abstract Factory pattern works, we are going to create a cross-platform GUI component application.

To start we are first going to create our first product hierarchy, the Button. We will ceate an IButton interface, the AbstractProduct participant, and three buttons LinuxButton, MacOSButton and WindowsButton, the ConcreteProducts:



namespace AbstractFactory.Buttons
{
    /// <summary>
    /// The Abstract Factory pattern assumes that we have several families
    /// of products, structured into separate class hierarchies (Button/Checkbox/Panel).
    /// All products of the same family have a common interface. 
    /// 
    /// This is the common interface for buttons family.
    /// </summary>
    public interface IButton
    {
        public void Paint();
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Buttons
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows).
    /// 
    /// This is a Linux variant of a button.
    /// </summary>
    public class LinuxButton : IButton
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a LinuxButton");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Buttons
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows).
    /// 
    /// This is a MacOS variant of a button.
    /// </summary>
    public class MacOSButton : IButton
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a MacOSButton");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Buttons
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows).
    /// 
    /// This is a Windows variant of a button.
    /// </summary>
    public class WindowsButton : IButton
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a WindowsButton");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Now we will create another set of Abstract Product and Concrete Products, the checkboxes:



namespace AbstractFactory.Checkboxes
{
    /// <summary>
    /// Checkboxes are the second product family.
    /// It has the same variants as buttons.
    /// </summary>
    public interface ICheckbox
    {
        public void Paint();
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Checkboxes
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows)
    /// 
    /// This is a variant of a checkbox.
    /// </summary>
    public class LinuxCheckbox : ICheckbox
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a Linux checkbox");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Checkboxes
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows)
    /// 
    /// This is a variant of a checkbox.
    /// </summary>
    public class MacOSCheckbox : ICheckbox
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a MacOS checkbox");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Checkboxes
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows)
    /// 
    /// This is a variant of a checkbox.
    /// </summary>
    public class WindowsCheckbox : ICheckbox
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a WindowsCheckbox");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

And the final product family, the Panels:



namespace AbstractFactory.Panels
{
    /// <summary>
    /// Panels is the third product family. It has the same variants as buttons and checkboxes.
    /// </summary>
    public interface IPanel
    {
        public void Paint();
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Panels
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows)
    /// 
    /// This is a variant of a panel.
    /// </summary>
    public class LinuxPanel : IPanel
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a LinuxPanel");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Panels
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows)
    /// 
    /// This is a variant of a panel.
    /// </summary>
    public class MacOSPanel : IPanel
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a MacOSPanel");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


namespace AbstractFactory.Panels
{
    /// <summary>
    /// All product families have the same varieties (Linux/MacOS/Windows)
    /// 
    /// This is a variant of a panel.
    /// </summary>
    public class WindowsPanel : IPanel
    {
        public void Paint()
        {
            Console.WriteLine("Successfully created a WindowsPanel");
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

The next step is to create our Abstract Factory participant. We will create an interface for creating an IButton, an ICheckbox and an IPanel.



using AbstractFactory.Buttons;
using AbstractFactory.Checkboxes;
using AbstractFactory.Panels;

namespace AbstractFactory.Factories
{
    /// <summary>
    /// Abstract factory knows about all (abstract) product types
    /// </summary>
    public interface IGUIFactory
    {
        public IButton CreateButton();
        public ICheckbox CreateCheckbox();
        public IPanel CreatePanel();
    }
}


Enter fullscreen mode Exit fullscreen mode

Now we can start implementing the Concrete Factory participants:



using AbstractFactory.Buttons;
using AbstractFactory.Checkboxes;
using AbstractFactory.Panels;

namespace AbstractFactory.Factories
{
    /// <summary>
    /// Each concrete factory extends basic factory and responsible for creating 
    /// products of a single variety
    /// </summary>
    public class LinuxFactory : IGUIFactory
    {
        public IButton CreateButton()
        {
            return new LinuxButton();
        }

        public ICheckbox CreateCheckbox()
        {
            return new LinuxCheckbox();
        }

        public IPanel CreatePanel()
        {
            return new LinuxPanel();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


using AbstractFactory.Buttons;
using AbstractFactory.Checkboxes;
using AbstractFactory.Panels;

namespace AbstractFactory.Factories
{
    /// <summary>
    /// Each concrete factory extends basic factory and responsible for creating 
    /// products of a single variety.
    /// </summary>
    public class MacOSFactory : IGUIFactory
    {
        public IButton CreateButton()
        {
            return new MacOSButton();
        }

        public ICheckbox CreateCheckbox()
        {
            return new MacOSCheckbox();
        }

        public IPanel CreatePanel()
        {
            return new MacOSPanel();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode


using AbstractFactory.Buttons;
using AbstractFactory.Checkboxes;
using AbstractFactory.Panels;

namespace AbstractFactory.Factories
{
    /// <summary>
    /// Each concrete factory extends basic factory and responsible for creating 
    /// products of a single variety
    /// </summary>
    public class WindowsFactory : IGUIFactory
    {
        public IButton CreateButton()
        {
            return new WindowsButton();
        }

        public ICheckbox CreateCheckbox()
        {
            return new WindowsCheckbox();
        }

        public IPanel CreatePanel()
        {
            return new WindowsPanel();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

The final step is to allow the Client application to get the appropriate products. The application doesn't care which concrete factory it will use. It just needs an interface to access the products:



using AbstractFactory.Buttons;
using AbstractFactory.Checkboxes;
using AbstractFactory.Factories;
using AbstractFactory.Panels;

namespace AbstractFactory.App
{
    /// <summary>
    /// Factory users don't care which concrete factory they use since they work with
    /// factories and products through abstract interfaces.
    /// </summary>
    public class Application
    {
        private IButton button;
        private ICheckbox checkbox;
        private IPanel panel;

        public Application(IGUIFactory factory)
        {
            button = factory.CreateButton();
            checkbox = factory.CreateCheckbox();
            panel = factory.CreatePanel();
        }

        public void Paint()
        {
            button.Paint();
            checkbox.Paint();
            panel.Paint();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Finally, lets create our Main() method:



using AbstractFactory.App;
using AbstractFactory.Factories;
using System.Runtime.InteropServices;

public class Program
{
    public static void Main(string[] args)
    {
        Application app = ConfigureApplication();
        app.Paint();
    }

    private static Application ConfigureApplication()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            return new Application(new LinuxFactory());
        if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            return new Application(new MacOSFactory());

        return new Application(new WindowsFactory());
    }
}


Enter fullscreen mode Exit fullscreen mode

And the output of the application:

Output of the exampe

Pros and Cons of Factory Method Pattern

✔ We can be sure that the products you’re getting from a factory are compatible with each other. ❌ The code may become more complicated than it should be, since a lot of new interfaces and classes are introduced along with the pattern.
✔ We avoid tight coupling between concrete products and client code.
✔ We can extract the product creation code into one place, making the code easier to support, thus respecting the Single Responsibility Principle.
✔ We can introduce new variants of products without breaking existing client code, thus satisfying the Open/Closed Principle.

Relations with Other Patterns

  • Many designs start by using the Factory Method and evolve toward Abstract Factory, Prototype or Builder.
  • Abstract Factory classes are often based on a set of Factory Methods.
  • Builder focuses on constructing complex object step by step. Abstract Factory specializes in creating families of related objects. Abstract Factory returns the product immediately, whereas Builder lets us run some additional construction steps before fetching the product.
  • Abstract Factory can serve as an alternative to Facade when we only want to hide the way the subsystem objects are created from the client code.
  • We can use the Abstract Factory along with the Bridge pattern. This pairing is useful when some abstractions defined by the Bridge can only work with specific implementations. In this case, Abstract Factory can encapsulate these relations and hide the complexity from the client code.

Final Thoughts

In this article, we have discussed what is the Factory Method pattern, when to use it and what are the pros and cons of using this design pattern. We also examined the relations between Abstract Factory and other patterns.

The Abstract Factory is a very useful design pattern to implement when working with a family of objects that a factory creates.

If you need to abstract away the details of object creation, or if you need to be able to change the types of objects that are created at runtime, this is an ideal pattern to use. With some thought and planning, the Abstract Factory design pattern can be a valuable addition to your toolkit.

It's worth noting that the Abstract Factory pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.

Top comments (0)