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.

Watch it evolve1 / 9
The problem

A checkout for two countries

Checkout adds US tax or shows German VAT as included, and formats money as dollars or euros.

80
Start
Stage 1
Stage 2
Final

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.

The first versionJava
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));
    }
}
«enum»CountryUSDECheckout− country: Country~ receipt(int): String− taxCents(int): int− taxIncluded(): boolean− format(int): String

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.

One decision, made three timesJava
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

$25.00

25,00 €

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.

Java
interface TaxRule {
    int taxCents(int subtotal);
    boolean included();
}

interface MoneyFormat {
    String format(int cents);
}
«create»«create»«create»«create»«interface»TaxRule+ taxCents(int): int+ included(): boolean«interface»MoneyFormat+ format(int): String«interface»RegionKit+ region(): String+ taxRule(): TaxRule+ moneyFormat(): MoneyFormatSalesTax− percent: int+ taxCents(int): int+ included(): booleanIncludedVat− percent: int+ taxCents(int): int+ included(): booleanDollarFormat+ format(int): StringEuroFormat+ format(int): StringUsKit+ region(): String+ taxRule(): TaxRule+ moneyFormat(): MoneyFormatGermanyKit+ region(): String+ taxRule(): TaxRule+ moneyFormat(): MoneyFormatCheckout− region: String− tax: TaxRule− money: MoneyFormat~ receipt(int): String
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.

«create»«create»«create»«create»«create»«create»«interface»TaxRule+ taxCents(int): int+ included(): boolean«interface»MoneyFormat+ format(int): String«interface»RegionKit+ region(): String+ taxRule(): TaxRule+ moneyFormat(): MoneyFormatSalesTax− percent: int+ taxCents(int): int+ included(): booleanIncludedVat− percent: int+ taxCents(int): int+ included(): booleanDollarFormat+ format(int): StringEuroFormat+ format(int): StringPoundFormat+ format(int): StringUsKit+ region(): String+ taxRule(): TaxRule+ moneyFormat(): MoneyFormatGermanyKit+ region(): String+ taxRule(): TaxRule+ moneyFormat(): MoneyFormatUkKit+ region(): String+ taxRule(): TaxRule+ moneyFormat(): MoneyFormatCheckout− region: String− tax: TaxRule− money: MoneyFormat~ receipt(int): String

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

RegionKit

One creation method per kind of product

Concrete factory

UsKit, GermanyKit, UkKit

Creates one matching family

Abstract product

TaxRule, MoneyFormat

What the client works with

Concrete product

SalesTax, IncludedVat, DollarFormat, EuroFormat, PoundFormat

What gets made

Client

Checkout

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, RegionKit gains 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. EuroFormat shouldn't know about IncludedVat; the kit is what ties them.
  • Use data where the families differ only in values. SalesTax takes 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 std::unique_ptr to each product

JavaScript

Any object with the creation methods; often a plain object of functions

Python

A Protocol for the kit; a module or a small class per family

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

1. The shop adds an address format that also varies by country. What changes in the kit version?
2. Why is a mismatched pair, US tax with euro formatting, impossible in the kit version?
3. The tax rule and the money format could vary independently, and any combination is valid. Is Abstract Factory the right tool?
4. What's the main difference between Abstract Factory and Factory Method?

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.