Low-level designDeep dive8 min
Abstract Factory
When several objects must always be chosen together, as a matching set, put the choice in one object whose job is to create the whole set. Code that uses the set asks that factory, and can never mix parts from two sets.
A checkout for two countries
Checkout adds US tax or shows German VAT as included, and formats money as dollars or euros.
1 · The situation
One checkout, two countries
An online shop sells to the US and Germany. The rules differ in two ways that have to agree. In the US, sales tax of 8% is added at checkout and prices are shown as $25.00. In Germany, 19% VAT is already included in the price and amounts are shown as 25,00 €. Checkout switches on the country for each decision.
Python writes _tax_cents() and the other helpers in snake case; the names are otherwise the same in all four languages. Amounts are in cents, with tax rounded to the nearest cent.
enum Country { US, DE }
class Checkout {
private final Country country;
Checkout(Country country) {
this.country = country;
}
String receipt(int subtotal) {
int tax = taxCents(subtotal);
int total = taxIncluded() ? subtotal : subtotal + tax;
String taxLabel = taxIncluded() ? "VAT included " : "tax ";
return country + ": subtotal " + format(subtotal) + ", " + taxLabel + format(tax)
+ ", total " + format(total);
}
private int taxCents(int subtotal) {
switch (country) {
case US: return (subtotal * 8 + 50) / 100;
default: return (subtotal * 19 + 59) / 119;
}
}
private boolean taxIncluded() {
switch (country) {
case US: return false;
default: return true;
}
}
private String format(int cents) {
switch (country) {
case US: return String.format("$%d.%02d", cents / 100, cents % 100);
default: return String.format("%d,%02d €", cents / 100, cents % 100);
}
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new Checkout(Country.US).receipt(2500));
System.out.println(new Checkout(Country.DE).receipt(2500));
}
}The class diagram of this version, drawn from its code: one checkout and a country enum.
US: subtotal $25.00, tax $2.00, total $27.00 DE: subtotal 25,00 €, VAT included 3,99 €, total 25,00 €
2 · The change
Three switches that must agree
The shop plans to open in the UK: 20% VAT included, prices in pounds. In this design that's a case in each of three switches, and the cases belong together. A checkout that adds US tax but shows euros, or shows pounds but adds tax on top, is a bug no compiler will catch: each switch is fine on its own; only the combination is wrong.
class Checkout {
private final Country country;
Checkout(Country country) {
this.country = country;
}
String receipt(int subtotal) {
int tax = taxCents(subtotal);
int total = taxIncluded() ? subtotal : subtotal + tax;
String taxLabel = taxIncluded() ? "VAT included " : "tax ";
return country + ": subtotal " + format(subtotal) + ", " + taxLabel + format(tax)
+ ", total " + format(total);
}
private int taxCents(int subtotal) {
switch (country) {
case US: return (subtotal * 8 + 50) / 100;
default: return (subtotal * 19 + 59) / 119;
}
}
private boolean taxIncluded() {
switch (country) {
case US: return false;
default: return true;
}
}
private String format(int cents) {
switch (country) {
case US: return String.format("$%d.%02d", cents / 100, cents % 100);
default: return String.format("%d,%02d €", cents / 100, cents % 100);
}
}
}Decision | US | Germany | Must match |
|---|---|---|---|
Tax amount | 8%, added | 19%, included | The included-or-added rule |
Tax shown as | "tax" | "VAT included" | The tax amount's rule |
Money format |
|
| The country's currency |
The practice checks flag it: Country is switched on in three places. What they can't see is the extra risk here, that the three answers form a set.
3 · The fix
One kit per region creates the whole set
Give each varying decision an interface, and give each region one object that creates its matching set:
TaxRule computes the tax and says whether it's included; MoneyFormat formats an amount. SalesTax and IncludedVat take the rate as a number; DollarFormat and EuroFormat format amounts.
interface TaxRule {
int taxCents(int subtotal);
boolean included();
}
interface MoneyFormat {
String format(int cents);
}US: subtotal $25.00, tax $2.00, total $27.00 DE: subtotal 25,00 €, VAT included 3,99 €, total 25,00 € UK: subtotal £25.00, VAT included £4.17, total £25.00
The US and Germany as before, and the UK from one new kit and one new format.
4 · The pattern
Abstract Factory: matching sets, chosen once
Abstract Factory provides an interface for creating families of related or dependent objects without naming their concrete classes. The client is given one factory, and everything it creates from that factory belongs together. Choosing the factory is the only decision, and it's made once.
The roles in the shop's code: RegionKit is the abstract factory, each kit a concrete factory, and the tax rules and formats are the products.
Role | In the shop | Its job |
|---|---|---|
Abstract factory |
| One creation method per kind of product |
Concrete factory |
| Creates one matching family |
Abstract product |
| What the client works with |
Concrete product |
| What gets made |
Client |
| Asks its factory for products and uses them |
The checkout takes both products from the one kit it was given, so they always match.
The checks don't have a separate detector for Abstract Factory, but they recognize what it's built from: each kit method is a factory method that the kits override with different products. An abstract factory is, in code, a set of factory methods on one object.
5 · What it costs
New families are cheap; new kinds of product are not
- Many types. Two decisions for three regions took two interfaces, five products, an interface for the kit and three kits.
- A new kind of product touches every kit. If the receipt also needs an address format,
RegionKitgains a method and every kit implements it. - Indirection. To learn how German tax is computed, a reader goes from the checkout to the kit to the tax rule.
Change | Switch version | Kit version |
|---|---|---|
A new region | A case in every switch | One new kit, plus any new products |
A new kind of product | One new switch | A new method in the interface and in every kit |
A mismatched set | Possible, and silent | Impossible through a kit |
If the decisions don't need to match, there's no family and no need for this pattern: inject each one separately. If there's one region and no plan for a second, the switch version is simpler. Reach for Abstract Factory when the set really must agree and new sets keep coming.
6 · Getting it right
Choose the kit once, at the edge
- Choose the factory in one place. The region's kit is picked where the order is created, from the shipping address, not inside
Checkout. - Hand the factory in. The client should receive its kit, not look one up; see Dependency injection.
- Keep products independent of each other's classes.
EuroFormatshouldn't know aboutIncludedVat; the kit is what ties them. - Use data where the families differ only in values.
SalesTaxtakes its rate as a parameter, so a new country with the same kind of tax needs no new tax class.
Language | The usual shape |
|---|---|
Java | An interface with one method per product, and a class per family |
C++ | An abstract class whose methods return |
JavaScript | Any object with the creation methods; often a plain object of functions |
Python | A |
7 · Look-alikes
Factories that create one thing, or a family
The creational patterns differ in what they create and who chooses.
Pattern | Creates | The choice is made by |
|---|---|---|
Abstract Factory | A family of products that must match | Which factory object the client is given |
Factory Method | One product, inside a class that uses it | A subclass overriding one method |
Builder | One complex product, in steps | The calls made on the builder |
Strategy | Nothing: it is the behavior | The client, choosing one way of doing a job |
Abstract Factory and Strategy look alike in code: an interface, several implementations, one given to a client. The difference is the job. A strategy does the work; a factory makes the objects that do it, and an abstract factory makes several that have to fit together.
8 · In practice
Families you've already used
Java's database API works this way. A Connection from a given driver creates statements, prepared statements and other objects, all from that same driver; code written against the interfaces never names a driver's classes, and can't mix one driver's statement with another's connection.
None of the practice problems needs matching families yet, and the checks report the factory methods an abstract factory is made of. Design the parking lot and notice which of its choices are independent, and which, if any, must agree.
Check yourself
4 questions
Takeaways
Remember this
- Abstract Factory creates families of related objects that must be used together.
- The client is given one factory; every product it gets comes from that single choice.
- Adding a family is one new factory; adding a kind of product changes every factory.
- If the choices don't need to match, inject them separately instead.
- An abstract factory is a set of factory methods on one object.