Abstract factory
A family of related objects created together.
01The intuition
The problem it solves
A school exam app renders question papers in two styles: a printed paper and an on-screen paper. Each style has its own heading widget, its own question box and its own answer box. The first version picks every widget separately.
def build_paper(style: str, questions):
if style == "print":
heading = PrintHeading()
else:
heading = ScreenHeading()
boxes = []
for q in questions:
if style == "print":
boxes.append(PrintQuestionBox(q))
else:
boxes.append(ScreenQuestionBox(q))
if style == "print":
answer = PrintAnswerBox()
else:
answer = ScreenAnswerBox()
return Paper(heading, boxes, answer)
Three if-elses that must agree. The bug waiting to happen is a mixed paper, a print heading with a screen answer box, and nothing in the code prevents it. Adding a third style, a Braille paper, means editing three ladders in step. The widgets come in families, and the program treats them as singles.
An abstract factory is an object that creates a whole family of related objects, so that everything it hands out matches. One factory per family; the caller holds one factory and cannot mix.
Build it one piece at a time
Step 1: write the promise for the family. One interface with a creation method per member of the family.
from abc import ABC, abstractmethod
class PaperWidgets(ABC):
@abstractmethod
def heading(self) -> Heading: ...
@abstractmethod
def question_box(self, q) -> QuestionBox: ...
@abstractmethod
def answer_box(self) -> AnswerBox: ...
Step 2: one concrete factory per style. Each knows only its own widgets, so mixing is impossible by construction.
class PrintWidgets(PaperWidgets):
def heading(self): return PrintHeading()
def question_box(self, q): return PrintQuestionBox(q)
def answer_box(self): return PrintAnswerBox()
class ScreenWidgets(PaperWidgets):
def heading(self): return ScreenHeading()
def question_box(self, q): return ScreenQuestionBox(q)
def answer_box(self): return ScreenAnswerBox()
Step 3: the builder of the paper takes one factory and never branches.
def build_paper(widgets: PaperWidgets, questions) -> Paper:
return Paper(
widgets.heading(),
[widgets.question_box(q) for q in questions],
widgets.answer_box(),
)
paper = build_paper(PrintWidgets(), questions)
Step 4: choose the family once, at the edge. A small simple factory maps the style name to the family factory. A Braille style is one new BrailleWidgets class and one registration.
FAMILIES = {"print": PrintWidgets, "screen": ScreenWidgets}
def widgets_for(style: str) -> PaperWidgets:
return FAMILIES[style]()
| Step | Ladders in build_paper | Can a mixed paper be built | Adding Braille edits |
|---|---|---|---|
| Start | 3 | yes, silently | 3 ladders |
| 1 and 2 | 3 (not yet used) | yes | one class plus 3 ladders |
| 3 and 4 | 0 | no | one class, one map entry |
Why it works
The family becomes a single value, the factory object, that travels through the program. Consistency is enforced by the type system rather than by discipline: there is no API through which a PrintWidgets can produce a screen box. The invariant is "every object created from one factory belongs to one family". Callers depend on the abstract interfaces for widgets and for the factory, so they are closed to new families.
Where it breaks: adding a new member to the family. If papers now need a footer, every concrete factory gains a method, which is the opposite of easy. Abstract factory is the right tool when families are added more often than members. When members change more often, prefer plain factories or a registry per member.
The trade-off you must say out loud
This is one of the heavier patterns: an interface plus a class per family, for a benefit that only appears when there are at least two families and at least two members. Say that explicitly: "I used it because print and screen widgets must never mix; with one style I would construct directly". The follow-up is often "what about shared widgets", and the answer is that a concrete factory may return the same class for a member two families share, which is fine.
Where it shows up in the 100 Google problems
- Design 2048: a renderer family (text, GUI) supplies matched tile, board and score views, so a test can run the whole game on the text family.
- Design an online shopping system: a payment provider family provides a matching authoriser, capturer and refunder; mixing a card authoriser with a wallet refunder would be a serious bug.
- Design a notification service: per channel, a matched template renderer and sender; the email family renders HTML, the SMS family plain text.
- Design a JSON parser: a node factory family lets the same parser build either a plain-dict tree or a typed node tree, depending on what the caller wants back.