Abstract Factories are next in our series on Design Patterns. This pattern makes the decision making and complexity in creating the right object at the right time more manageable and reliable.
Let’s start with an analogy. As an astute entrepreneur you decide to get into the wild world of chair making. With big ambitions you decide to sell modern chairs, Victorian chairs, abstract chairs, simple chairs, and really — all the chairs. Not only that, but chairs will be made to order. Genius!
One after another, customers start walking in the front door and you do your best to sell them on how amazing your chairs are. They seem convinced, but aren’t sure which chair is best for them. You realize you’re not sure, either. You just assumed they would know!
By the end of the day, you’re tired. Every sale took tremendous effort. Contemplatively, you sit down in your favorite chair and reflect. Then, an idea! You should have a process for determining which chair a person needs! You could give them a tablet to answer some questions and give you all the information you need to give them the perfect chair. It’s just crazy enough to work!
Welcome to the Abstract Factory Design Pattern
That fun little anecdote describes the problem and solution for which the Abstract Factory design pattern is intended. Sometimes, when developing, we know exactly what we need and can generate it right then and there: new PerfectClass(). But other times what you need is surrounded by a bunch of conditions. And, while you could work out those conditions every time, all you really want in this moment is the right object.
The Abstract Factory houses these conditions and then returns the appropriate class instance based on the criteria. Now all of your conditions are in one place.
This is a good example of the Single Responsibility Principle, which states that every module, class, and function should only have one functional responsibility. Here the factory only does one thing: It figures out which class to make and then makes it. The code that’s using the factory shouldn’t be figuring this out as that can quickly become functionality in and of itself. Meaning, the implementing code should only tackle the responsibility of how to use the class, not the responsibility of selecting the right one. As in our example, the salesman should figure out the right chair based on the information the customer provided.The customer should know what to do with the chair once they get it and where to get chairs. Makes sense, right?
The making of a ChairFactory
Ok, let’s take a look at what a ChairFactory could be and talk through it.
Here’s how we’d use this:
// First make our factory $chairFactory = new ChairFactory(); // Now make our chairs! $simpleChair = $chairFactory->createChair(true, false); $rockingChair = $chairFactory->createChair(false, true); $modernChair = $chairFactory->createChair(true, true); // Throw an exception when the criteria doesn't make sense $impossibleChair = $chairFactory->createChair(false, false); // We know we can get the price of any chair $simpleChair->getPrice(); // 79.99 $modernChair->getPrice(); // 149.99 $rockingChair->getPrice(); // 199.99
Ok, let’s point out a few different concepts here:
First, every chair implements the ChairInterface interface. This gathers together all of our commonality between the chairs and enforces that each chair must have the ChairInterface::getPrice() method.
Second, this is important as the ChairFactory::createChair() method returns the interface. This means that no matter what chair is returned, the code asking for the chair can be sure that it will at least have a price. This allows one to total up the prices or do whatever else needs to be done without caring about anything else.
Third, the chair classes may still have more methods than the interface enforces. And the code retrieving the chair may check which type of chair it is and then use those methods. Even in checking the calling code didn’t worry about how the chair was selected, just what it may do with it now that it has it. In other words, it only worries about implementation, not creation.
Fourth, as mentioned in the Value Object article, it’s good to throw an exception if things just don’t make sense. That way you don’t get a strange problem down the road. Instead, you can immediately see what conditions led to the issue.
Key takeaways for the Abstract Factory pattern
Let’s pull out some of the core principles of this pattern:
- Let the factory manage the conditions of the object creation. If there is complexity around which class should be instantiated, then you probably should make a factory.
- Use the same interface for the classes that may be returned. Take all the commonalities and requisites for the class implementation and put them in the interface so you know that no matter what is returned it may be used safely.
- Don’t mix creation with implementation. The code that wants the class instances should only worry about how the instances should be used and work together. The factory should only worry about which class should be instantiated and what it needs to do so.
A lot of programming comes down to recognizing the proper assignment of responsibility. It’s all too easy to make a simple function, then add some conditions, then extend the functionality, and finally you have a “god function” that does far too much. Testing such a function is incredibly difficult and unreliable.
The Abstract Factory Design Pattern is a great way to take the complexity of object creation out of your implementing code. It’s easy to not think of at first, but creation is a type of functionality. So if you ask yourself whether your function is both creating and implementing, it should probably only do one of those things, not both.
You can even take a step further and make an interface for your factory and create multiple factories — make a product interface, and now add sofas and loveseats to expand the empire!