Severin Perez

Thematic Configuration with the Abstract Factory Pattern

October 17, 2018

In object-oriented design, one of the principle aims is to produce code that is flexible, maintainable, and reusable. One of the ways to do this is to use abstractions in your code rather than concretions. The more your objects know about how one another are implemented, the more dependencies there are in your system. As the number of dependencies grows, the potential for cascading breakage grows as well. But what happens when you have a system that requires certain objects to come from the same family? How do you ensure that any objects you instantiate are indeed from that family without hard-coding a complicated control structure? One solution to this problem is the abstract factory pattern (AFP). Let’s dive in and see how it works.

Thematic Configuration

The AFP originates with the so-called “Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides), which wanted to specify a means of encapsulating object factories thematically without having to specify any concretions. To do this, the group created the AFP, which they intended to be used to:

“Provide an interface for creating families of related or dependent objects without specifying their concrete classes.” [1]

At first blush, this sounds a bit esoteric, but the idea of using thematically linked classes makes more sense when you think of it as a configuration mechanism. The classic example of the need that the AFP addresses relates to operating systems. Imagine a piece of software that is designed to be portable between different operating systems: Mac; Windows; Linux; etc. Now imagine that this software needs to instantiate different kinds of widgets to do its work. When in a Windows environment, this software should certainly instantiate Windows-compatible buttons and dialogue boxes rather than Mac-compatible versions (and vice versa). Without configuration, your software might inadvertently use the wrong type of widget and cause unexpected errors.

One way to solve the configuration problem would be to hard-code test conditions in your software to see whether it is currently in a Windows, Mac, or Linux environment, and then instantiate the right kind of widget accordingly, as in the following pseudo-code:

function InstantiateWidget(environment) {
  if (environment == “Windows”)
    return new WindowsWidget;
  else if (environment == “Mac”)
    return new MacWidget;
  else if (environment == “Linux”)
    return new LinuxWidget;
  else
    throw new IncompatibleEnvironmentError();
}

The above might seem like a reasonable approach, and indeed, in a small program it may very well be fine. However, there are a number of problems with this approach. First, if you later want to add a new environment, such as iOS or Android, then you will have to edit this function to include new conditionals. This would be a clear violation of the Open/Closed Principle. Second, in all likelihood you will be making more than one kind of widget in a program (windows, checkboxes, etc.) and all of your functions will thus be littered with conditionals of this sort. Finally, as a result of this structure, your program will be dependent on every kind of widget no matter where it is being used. As your software grows, code like this will quickly become impossible (or at least unpleasant) to manage.

The Abstract Factory Pattern

In order to address the problems raised by thematic configuration through conditionals, the AFP proposes a different approach. Rather than concern itself with thematically varying dependencies directly, clients should instead rely on abstractions. These abstractions take two forms: the abstract services the client intends to use (widgets, etc.); and, an abstract factory to instantiate those services. In this case, the client is divorced entirely from the service creation process. Not only does the client not know what services it is using but it doesn’t care. The client only cares that a given service implements an expected set of behaviors.

Before we look at this pattern in more detail, let’s consider the actual purpose of the abstract factory for which the pattern is named. An abstract factory defines the contract that concrete factories will follow. That contract includes specification for the construction of various services. Notably, the contract defined by the abstract factory itself includes abstractions. In other words, the abstract factory exists only to encapsulate a group of thematically similar functions without necessarily knowing how they are implemented or what they do. To continue with the classic example, the abstract factory does not know about WindowsWidgets and MacWidgets, it only knows about abstract Widgets and will expect its descendent concrete factories to know how to implement those concrete widgets in accordance with its particular theme. In following this pattern, a client can accept an abstract factory of any sort (Mac, Windows, etc.) and use it to create thematically appropriate widgets as needed.

Due to its various moving parts, developing a mental model for how the AFP works can be a bit of a challenge. At its core though, the AFP requires just a few elements:

  • Client: The entity that will eventually make use of whichever abstract factory and product interfaces it is provided with as configuration.
  • Abstract Factory: An interface that describes the contract that concrete factories will use to create services. Given that the abstract factory does not know details about those services, it will declare its contract in abstract terms, leaving the concrete factories to declare specifics.
  • Concrete Factory: A concrete implementation of the abstract factory, which can be used to instantiate concrete services. Each of those services will fit together thematically and should be designed to work with one another as needed. Many concrete factories can be defined, with one of them eventually being used to configure the activities of the client.
  • Abstract Services: An interface that describes the contract of an abstract service. Both the abstract factory and the client will be aware of these abstract services, but not of specific implementations.
  • Concrete Services: Thematically-consistent implementations of the various abstract services.

Building Monuments

Per usual, the best way to illustrate a design pattern is through actual code. In the following example, our task is to write a program that can build different kinds of thematically-appropriate monuments depending on how a client is configured (that is, which factory it is provided). Take a moment to review the following code and see if you can pick out the roles that each of the entities is playing. Afterwards, we’ll go through it together.

using System;

namespace abs_fac_1
{
    class Program
    {
        static void Main(string[] args)
        {
            MonumentHandler greekMonumentHandler = 
                new MonumentHandler(new GreekMonumentFactory(), "Athena", "Pericles");

            greekMonumentHandler.IssueMessages();
                // The Olympian deity Athena demands tribute!
                // The noble Greek leader Pericles requests submission of taxes!

            MonumentHandler egyptianMonumentHandler =
                new MonumentHandler(new EgyptianMonumentFactory(), "Sekhmet", "Hatchepsut");

            egyptianMonumentHandler.IssueMessages();
                // Now accepting offerings to the Egyptian deity Sekhmet!
                // The mighty Egyptian pharaoh Hatchepsut demands payment of taxes!
        }
    }

    class MonumentHandler
    {
        private IAbstractMonumentFactory Factory;

        private ITemple Temple;

        private IPalace Palace;

        public MonumentHandler(IAbstractMonumentFactory factory, string deityName, string leaderName)
        {
            this.Factory = factory;
            this.Temple = factory.BuildTemple(deityName);
            this.Palace = factory.BuildPalace(leaderName);
        }

        public void IssueMessages()
        {
            this.Temple.CollectOfferings();
            this.Palace.CollectTaxes();
        }
    }

    interface IAbstractMonumentFactory
    {
        ITemple BuildTemple(string deityName);

        IPalace BuildPalace(string leaderName);
    }

    class GreekMonumentFactory : IAbstractMonumentFactory
    {
        public ITemple BuildTemple(string deityName) => new GreekTemple(deityName);

        public IPalace BuildPalace(string leaderName) => new GreekPalace(leaderName);
    }

    class EgyptianMonumentFactory : IAbstractMonumentFactory
    {
        public ITemple BuildTemple(string deityName) => new EgyptianTemple(deityName);

        public IPalace BuildPalace(string leaderName) => new EgyptianPalace(leaderName);
    }

    interface ITemple
    {
        void CollectOfferings();
    }

    class GreekTemple : ITemple
    {
        private string Deity;

        public GreekTemple(string deityName) => this.Deity = deityName;

        public void CollectOfferings() =>
            Console.WriteLine($"The Olympian deity {this.Deity} demands tribute!");
    }

    class EgyptianTemple : ITemple
    {
        private string Deity;

        public EgyptianTemple(string deityName) => this.Deity = deityName;

        public void CollectOfferings() =>
            Console.WriteLine($"Now accepting offerings to the Egyptian deity {this.Deity}!");
    }

    interface IPalace
    {
        void CollectTaxes();
    }
    
    class GreekPalace : IPalace
    {
        private string Leader;

        public GreekPalace(string leaderName) => this.Leader = leaderName;

        public void CollectTaxes() =>
            Console.WriteLine($"The noble Greek leader {this.Leader} requests submission of taxes!");
    }
    
    class EgyptianPalace : IPalace
    {
        private string Leader;

        public EgyptianPalace(string leaderName) => this.Leader = leaderName;

        public void CollectTaxes() =>
            Console.WriteLine($"The mighty Egyptian pharaoh {this.Leader} demands payment of taxes!");
    }
}

That was a lot of code to sort through for a simple example! (And an illustration of the overhead necessary to use the AFP.) For the sake of simplicity, our various monuments don’t do much, but if we look at each element we can see how they fit into the AFP.

  • Client: Our clients in this case are the MonumentHandler objects instantiated on lines 9 and 16 respectively. As a client, the MonumentHandler class doesn’t know about any concrete factories or services and therefore doesn’t depend on any concretions. Rather, it is aware of an abstract factory ( IAbstractMonumentFactory ) and two abstract services ( ITemple and IPalace ). It expects any concrete versions of these abstractions to follow a certain contract, which it uses to call methods in its constructor method and its IssueMessages method.
  • Abstract Factory: The IAbstractMonumentFactory acts as our abstract factory. It declares an interface that specifies the methods that any concrete factory should implement, including their abstract return types.
  • Concrete Factory: The GreekMonumentFactory and the EgyptianMonumentFactory both implement IAbstractMonumentFactory. However, unlike either the abstract factory or the client, these concrete factories are aware of concrete services. In this manner, each concrete factory is able to encapsulate related concrete services for use when eventually passed to the client.
  • Abstract Services: The ITemple and IPalace interfaces define the contract that any concrete services must follow. These abstract services are known to both the client, which will use those abstractions to access certain behavior, and the abstract factory.
  • Concrete Services: The GreekTemple, EgyptianTemple, GreekPalace, and EgyptianPalace each implement their respective abstractions. These classes define the actual behavior of objects created within a particular family. Owing to the fact that they are only known to their corresponding concrete factory, these concrete services will always be used together and there is no need for the client to know how to instantiate them directly.

When we instantiate and configure our two clients—greekMonumentHandler and egyptianMonumentHandler—we do so using the appropriate concrete factory. And indeed, when we then use those clients to execute certain behavior, we get thematically-appropriate results.

Benefits and Drawbacks

For a simple program such as the one above, the AFP may seem like an overreaction. Indeed, it’s not appropriate to every situation because it requires greater code overhead upfront. However, we also get the benefit of decoupling our client from the services it uses and the creation of those services. This allows for easy extension because we could create many different kinds of concrete factories to achieve different behavior. In the above example, we might want to create an AztecMonumentFactory and a RomanMonumentFactory in order to support new configurations for our client MonumentHandler. Doing so would be straightforward; however, as the number of concrete factories grows our ability to update them easily diminishes. If, for example, we wanted to add a BuildMausoleum function to our configuration, then we would have to add an implementation of that function to every single concrete factory.

Another benefit to consider is the fact that clients can be configured dynamically and at runtime. A client doesn’t need to know which factory it uses, it only needs to know about the abstract factory. As a result, we can change configuration by pointing the client towards a different concrete factory, thus changing its behavior.

On the whole, the AFP is a great way to insulate clients from the specifics of how their services are created. It’s also a useful means of configuring a client with a family of related objects and behaviors. As with any design pattern though, it’s important to consider trade-offs before choosing a course of action.

TL;DR

The abstract factory pattern (AFP) is a means of encapsulating related objects into a single configuration. This is accomplished through the use of an abstract factory that is aware of abstract services and concrete factories with implementations of the requisite services. Upon instantiation, a concrete factory may be used to configure the behavior of a client object. By using the AFP, it is possible to ensure the thematically-appropriate use of various types of services by a given client. Simultaneously, you can decouple that client from the specifics of its services. Although the AFP is a useful pattern in many situations, it isn’t appropriate for every situation due to upfront overhead and the difficulty of adding new services.

References


And that’s the abstract factory pattern! If you’re interested in learning more about design patterns, you can check out these articles on the strategy pattern and the decorator pattern, and/or stay tuned for future articles!


Note: This article was originally published on Medium.


You might enjoy...


© Severin Perez, 2021