What is it all about?
A design pattern is a generic, reusable solution to a commonly encountered problem within a specific context in software design. It is not a finished design that can be transformed directly into code. Instead, it is a description or template for how to solve a problem that can be used in many different situations.
Design patterns provide tested and proven development paradigms, which can speed up the development process. For effective software design, consideration of issues that might not become visible until later in the implementation is required. Reusing design patterns assists programmers and architects who are familiar with these concepts in avoiding subtle issues that can lead to major problems and making code easier to read and understand.
Also, design patterns allow developers to communicate using well-known, well-understood names for software interactions. Common design patterns can be improved over time, making them more robust than ad-hoc designs.
Abstract Factory is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. It separates the details of object creation from their usage so that our code isn’t dependent on the types of objects that are already defined and won’t need to be altered when we add a new type of object.
A real-life example
An example of how this pattern might be applied to a furniture shop application is in the creation of objects that represent:
- A family of related products including armchairs, sofas, and coffee tables.
- Several variants of this family. For example, the above-mentioned products are available in some variants like Modern, Classic and Retro.
On a Client’s request, we need to create individual furniture objects so that they match other objects of the same family. Also, we don’t want to change existing code when adding new products or families of products to the application.
The Abstract Factory pattern first suggests explicitly declaring interfaces for each product in the product family (e.g., Armchair, Sofa, or CoffeeTable). Then we can make all variants of products follow those interfaces. For example, all Armchair variants can implement the Armchair interface; all Sofa variants can implement the Sofa interface, and so on.
Next, we’ll declare the Abstract Factory; this is an interface with a list of methods to create the products that are part of the product family (for example, CreateArmchair, CreateSofa and CreateCoffeeTable). These methods must return abstract product types represented by the previously extracted interfaces, such as Armchair, Sofa, CoffeeTable, and so on.
Now, what about the different product variants? For each variant of a product family, we create a specific factory class based on the FurnitureFactory interface. A factory is a class that returns products of a particular kind. For example, the ClassicFurnitureFactory can only create an Armchair, a CoffeeTable and Sofa objects.
The Client’s code will use both factories and products through their abstract interfaces to start making a product that belongs to a certain family. This allows us to change the type of factory that we pass to the Client‘s code, as well as the product variant, without breaking the existing implementation.
But what creates the factory objects if the Client is only exposed to the abstract interfaces? Typically, during initialization, the application creates a concrete factory object, which is then passed as an argument to the Client’s method that creates products (s).
As a result, the overall diagram looks as follows: (some parts omitted for brevity).
The essence of it
So, to summarize this design pattern:
- Abstract Products declare interfaces for a set of distinct but related products which make up a product family.
- Concrete Products are various implementations of abstract products, grouped by variants. Each abstract product (armchair, sofa, etc.) must be implemented in all given variants (Classic, Modern, etc.).
- The Abstract Factory interface declares a set of methods for creating each of the abstract products.
- Concrete Factories implement the creation methods of the abstract factory. Each concrete factory corresponds to a specific variant of products and creates only those product variants.
- Although concrete factories instantiate concrete products, signatures of their creation methods must return corresponding abstract products. This way the Client’s code that uses a factory doesn’t get coupled to the specific variant of the product it gets from a factory. The Client can work with any concrete factory/product variant, as long as it communicates with their objects via abstract interfaces.
We should use the Abstract Factory when our code needs to work with different families of related products, but we don’t want it to depend on the concrete classes of those products (depend on abstractions rather than implementations; see the Dependency Inversion Principle). They might be unknown beforehand, or we simply want to allow for future extensibility.
Even though the code may become complicated as a result of using this design pattern, there are numerous benefits in terms of code quality:
- The products (objects) that we’re getting from a factory are compatible with each other.
- Tight coupling between concrete products and the Client’s code is avoided.
- It is easier to maintain the code because we can extract all of the code required to make products in one place (Single Responsibility Principle).
- We can introduce new product variations without breaking the existing Client’s code (Open/Closed Principle).
That was all for the Abstract Factory Design Pattern. The source code for the presented sample can be found on github.