Home Design Patterns Abstract Factory
Abstract Factory
Creational Gang of Four“Provide an interface for creating families of related or dependent objects without specifying their concrete classes.”
Gamma, Helm, Johnson & Vlissides, p. 87Purpose
An Abstract Factory hands out matched sets of objects. You ask it for a planner and a gripper driver, and because both came from the same factory they are guaranteed to belong together. The client never writes the name of a concrete class, so it never gets the chance to pair one from column A with one from column B.
That guarantee is the whole point, and it is worth separating from the more
obvious benefit of hiding constructor calls. Plenty of patterns hide a
new. This one encodes a constraint the type system cannot otherwise
express: these objects only make sense together.
Design
There are two hierarchies and they run in parallel. One abstract factory interface declares a creation method per product kind, and one concrete factory per family implements all of them. Alongside it sits an abstract type for each product kind, with one concrete product per family. A family is therefore a column through the product hierarchies, and a concrete factory is the object that knows which column it is standing in.
creates arrows never cross. That is the
invariant the pattern buys you: ConcreteFactory1 cannot hand back a
ProductA2, because there is nowhere in the code where the two family names
meet.The client is usually handed its factory once, at startup, and holds it for its whole life. Everything downstream of that single decision is written against abstract types only. If you find a concrete product name anywhere below the composition root, the pattern has already leaked.
Example
Every Autobot Robotics arm needs a set of software pieces that fit it: a motion planner that knows the kinematics, a gripper driver that knows the tool mount, and a calibration profile that knows the joint limits. The Arm601, Arm802 and Arm1210 each need their own version of all three, and the three pieces make assumptions about each other.
A field engineer commissioning a cell built a configuration that loaded the Arm601 kinematics alongside the Arm802 gripper driver. Nothing rejected it. The arm moved, the gripper opened and closed, and the tool center point was wrong by the difference in mount height between the two models. It ran for most of a shift, placing boards slightly out of position, before anyone worked out why the reject rate had climbed.
The fix was to stop letting the three pieces be chosen independently. An
ArmToolkit interface declares one creation method per piece, and
there is exactly one concrete toolkit per arm model. The cell controller asks the
toolkit for what it needs and no longer contains the string "Arm601" anywhere.
Choosing a mismatched pair is not a configuration error any more; it is not a
thing that can be written down.
interface ArmToolkit
{
MotionPlanner createPlanner();
GripperDriver createGripper();
}
class Arm601Toolkit : ArmToolkit
{
public MotionPlanner createPlanner() { return new Arm601Planner(); }
public GripperDriver createGripper() { return new Arm601Gripper(); }
}
class Arm1210Toolkit : ArmToolkit
{
public MotionPlanner createPlanner() { return new Arm1210Planner(); }
public GripperDriver createGripper() { return new Arm1210Gripper(); }
}
class CellController
{
MotionPlanner planner;
GripperDriver gripper;
public CellController(ArmToolkit kit)
{
planner = kit.createPlanner();
gripper = kit.createGripper();
}
public void runJob(Job j)
{
// plan and execute, never naming an arm model
}
}
class Startup
{
public void main(string model)
{
ArmToolkit kit = (model == "1210") ? new Arm1210Toolkit()
: new Arm601Toolkit();
CellController cell = new CellController(kit);
}
}
CellController constructor. Both
objects come out of the same kit, so the mismatch that cost a
shift of production has no syntax. The one place a model name still appears is
Startup, which is where it belongs.Seen in the Wild
The clearest example in a standard library is .NET's
System.Data.Common.DbProviderFactory: a provider supplies one
factory whose CreateConnection(), CreateCommand() and
CreateParameter() return objects from a single matched set, so you
cannot attach a SQL Server parameter to an Oracle command. JDBC does the same
thing one level down, where a java.sql.Connection acts as the
factory and every Statement it produces belongs to that driver.
Java's Swing look-and-feel mechanism is another: a LookAndFeel
supplies a full set of matching ComponentUI delegates, which is why
you get a consistent appearance rather than a Metal button next to a Motif
scrollbar.
Comparisons
- Factory Method is usually how the individual creation methods get implemented. The difference in scope is the point: a Factory Method makes one object, an Abstract Factory makes a set.
- Builder also produces complicated objects, but it assembles one object over many calls. An Abstract Factory returns finished objects one call at a time.
- Prototype is an alternative implementation strategy. Instead of a factory subclass per family, keep a set of configured instances and clone them. That trades classes for data.
- Bridge pairs with it naturally. An Abstract Factory is a good way to decide which Implementor an Abstraction gets handed.
- Singleton is how the factory often gets reached, and it is worth resisting. Pass the factory in; see that page for why.
- Dependency Injection covers much of the same ground in modern code. A DI container configured with one set of bindings per environment is an Abstract Factory with the wiring moved out of the language and into configuration.
Why It Exists
My take
Abstract Factory exists because of a gap in what type systems can say. We can express "this must be a MotionPlanner" easily. We cannot express "this MotionPlanner and that GripperDriver must have come from the same family", which is a far more common requirement and a far more expensive one to get wrong. The pattern closes that gap structurally: it removes the syntax you would need in order to make the mistake.
Without it, the family choice does not disappear, it just gets distributed. You end up with a dozen places that each ask which model of arm this is and each construct one piece accordingly. Every one of those is correct on the day it is written. The failure comes later, when a fourth model is added and eleven of the twelve get updated. I have never seen a codebase lose this fight all at once; it is always the twelfth site.
The honest cost is a rigidity that runs the other way. Adding a whole new family is free: write one more concrete factory and nothing else changes. Adding a new kind of product is expensive: the interface grows a method and every concrete factory in the system has to implement it. Before reaching for this pattern, work out which of those two things you expect to happen more often. If the answer is "new product kinds", you have picked the wrong axis, and the pattern will fight you for as long as it is in place.
In embedded work the families are hardware variants, and they arrive on a schedule set by the mechanical team rather than by you. That makes the trade-off unusually easy to evaluate: the set of product kinds is fixed by physics, and the set of families grows every eighteen months. That is exactly the shape this pattern is good at.
Criticisms
The class count is real and it is not small. Three product kinds and four families is twelve concrete products, four factories, three abstract product types and one factory interface: twenty types before a single line of behavior. When two of the four families differ in one method, that arithmetic starts to look like an elaborate way of avoiding a conditional.
It is also unhelpful when the families are not truly parallel. Real hardware does not cooperate. The Arm1210 grows a force sensor the other two do not have, and now the factory interface either gains a method that two implementations return null from, or the client starts type-testing the products it was given, which defeats the entire arrangement. The pattern assumes a clean grid, and product lines are rarely a clean grid.
Finally, it hurts to debug. A stack trace shows you createPlanner()
returning something, and the concrete type is decided somewhere you cannot see
from where you are standing. On a machine that is currently holding a part in the
air, "which planner am I actually running" is a question you want answered in
seconds, and this pattern adds a layer between you and the answer. Log the
factory's identity at startup; you will want it.