π Abstract Factory
You ever showered, and were lost in thought about how to create families of related objects? Fear not, Abstract Factories are here to help.
π Real-World Analogyβ
Abstract Factory can be compared to a pastry shop that produces different types of products. Just like how a pastry shop has standardized processes for making their products, an Abstract Factory has a standardized process for creating objects.
The pastry shop has different products, but all of them are within the same family. Products can be categorized into certain abstract objects (e.g., Cake) and concrete objects (e.g., Wedding Cake)
Other examples would be creating different types of buttons, or different types of vehicles.
For each concrete object, there is a certain βcookbook recipeβ for creating them. In a pastry shop context, it would be making batters, forming cakes, baking and decorating.
Similarly, an Abstract Factory has different factories for creating families of related objects. Just like how a pastry shop can produce different types of pastries and variants with the same standardized process, an Abstract Factory can produce different types of objects.
π€ Problemβ
Imagine working on a Christmas gift for your kids: A game where they run a pastry shop. Your code will consist of classes that represent:
- A family of related objects, say: Cake + Pie + Tart. (Yes, these are completely different things)
- Variants of objects of said family, say: Cake + Pie + Tart may be available in multiple variants: Wedding, Birthday, Graduation.
You need to create individual products that match others within the same family. Imagine: Your customers might be mad, if they receive products that do not match the occasion or that cannot be eaten at all.
In addition, new products or variants might be added frequently. You donβt want to touch the existing code all over again.
π‘ Solutionβ
First, the Abstract Factory pattern is about explicitly declaring interfaces for distinct products within the family (e.g. Cake, Pie and Tart). Then, you add variants of products that follow that interface. For example, you can add variants for Wedding, Birthday and Regular cakes, and so on.
Next, you need to create the Abstract Factory βan interface with creation methods for all products within the product family (e.g. createCake(), createPie() and createTart()). The return value must be abstract product types represented by the previously declared interfaces: Cake, Pie and Tart.
Now, we have the individual products within the family. But how about the specific product variants? For each of the variants, we create a separate Factory class based on the Abstract Factory interface. Itβs going to be a factory class that returns products of a specific kind. For example, the BirthdayFactory can only create products related to birthdays.
The client now needs to work with both factories and the products via their abstract interfaces. Like that, you can change the type of factory during runtime, by passing it as an argument. That way, you can select the product variant that the business logic receives without breaking it.
If the business logic wants a factory to produce a Cake, it does not need to be aware of the Factory class, nor does the kind of Cake matter. Whether itβs a Birthday Cake or a Wedding Cake, the business logic treats all cakes the same way, using their abstract ICake interface. Like that, the business logic only knows about the Cake, that it implements getEaten() in some way.
Hereβs still one more thing to explain: The Factories need to be created in some way. Usually, the business logic is creating these Factories upon initiation. But the business logic still needs to figure out which Factory is the right one, depending on a manual configuration or environment settings.
ποΈ Structureβ
- Abstract Products Declares a commonly shared interface for a set of similar but slightly different products β these make up the family.
- Concrete Products The concrete implementations. Each product within the family must have a concrete implementation in all variations.
- Abstract Factory Declares commonly shared interfaces for the different types of Factories.
- Concrete Factories Implements methods for creating each of the abstract products.
- Business Logic Used for instantiating, referencing and selecting the concrete Factories.
public abstract class ProductA
{
public abstract void Operation();
}
public class ConcreteProductA1 : ProductA
{
public override void Operation()
{
Console.WriteLine("Concrete product A1 implemented");
}
}
public class ConcreteProductA2 : ProductA
{
public override void Operation()
{
Console.WriteLine("Concrete product A2 implemented");
}
}
public interface IAbstractFactory
{
public abstract ProductA CreateProductA();
}
public class FactoryA1 : IAbstractFactory
{
public override ProductA CreateProductA()
{
return new ConcreteProductA1();
}
}
public class FactoryA2 : IAbstractFactory
{
public override ProductA CreateProductA()
{
return new ConcreteProductA2();
}
}
How to implementβ
- Make a list of all types of products and their variations.
- Use abstract product interfaces to define the making of each product. Implement specific product classes implementing these interfaces.
- Create an abstract factory and its methods for all abstract products.
- Make concrete Factory classes for each product variant.
- Define the factory initialization and selection process. Instantiate each concrete factory class once. Pass it to all places in your business logic where products are being constructed.
- Replace direct product constructor calls with the appropriate factory creation methods.
Pros & Consβ
β Prosβ
β Compatibility. Objects from the factory will be compatible with each other.
β Avoiding tight coupling. between concrete products and the client logic.
β Single Responsibility Principle. The product creation code can be extracted into one place, making the code easier to maintain.
β Open/Closed Principle. New product variants can be introduced without breaking the existing business logic.
β Consβ
β Might overcomplicate things. The code might become more complicated than it could be. A lot of additional interfaces and classes are produced for this pattern β which might not be needed. Factory Method might be a more lightweight choice to start out.
β Inflexibility. The abstract factory pattern can be less flexible than other creational patterns since changing the product families or adding new products might require modifying the abstract factory interface and all its implementations.
β Harder to understand. The abstract factory pattern can be more difficult to understand and implement than other creational patterns, especially for developers who are new to design patterns. But, donβt worry β thatβs why weβre here.
π Relation with other patternsβ
- Factory Method is how designs usually start β over time they might evolve to Abstract Factory, Prototype or Builder.
- Abstract Factory, Builders and Prototypes can be implemented as Singleton.
- Abstract Factory classes are frequently based on Factory Method themselves, but it is also possible to use Prototype for composition of the class methods.
- Abstract Factory can be an alternative to Facade if your intention is to hide how specialized objects are initialized.
- Builder constructs very complex objects step by step. On the other side, Abstract Factory is used for families of related objects.
- Abstract Factory can be paired with Bridge when some structures can only work with specific implementations. In that case, Factory Method is used to hide complexity.