Loslegen
Javaneer
Zurück zur Stufe
Stufe 1·Design Patterns angewandt

Factory & Builder

Erzeugungsmuster: Factory Method und Abstract Factory zentralisieren die Objekterzeugung, und der Builder konstruiert komplexe Objekte (einen Report, eine Transaction) Schritt für Schritt.

15 Min. LesezeitFortgeschritten

Deutsche Übersetzung in Arbeit

Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.

Auf dieser Seite

Creational patterns decouple your code from the messy details of making objects. Two earn their keep constantly: the Factory (in its method and abstract forms) centralizes the decision of which concrete class to create, and the Builder constructs complex objects step by step without a monstrous constructor. Both attack real smells in ledger-legacy - scattered new statements and telescoping constructors.

The problem with scattered new

Every new PostgresTransactionDao() sprinkled through the code hard-wires a concrete class at that spot - the Dependency Inversion problem, but for construction. When creation also involves a decision ("which fee policy for this transaction type?"), that logic gets copy-pasted everywhere a policy is needed.

Factory Method centralizes the choice in one place:

class FeePolicyFactory {
    FeePolicy forType(Type type) {
        return switch (type) {
            case DOMESTIC -> new DomesticFee();
            case INTL     -> new IntlFee();
            case CRYPTO   -> new CryptoFee();
        };
    }
}

Now the "which class" decision lives in exactly one spot. Callers ask the factory and receive a FeePolicy, never naming the concretes. (Note the tension with Open/Closed - a switch here still changes when types are added. That's an acceptable, contained trade-off: one switch in a factory beats the same switch scattered across the codebase, and it's the seam you'd later replace with registered strategies.)

Abstract Factory goes one level up: it creates families of related objects that must be used together. ledger-legacy must export reports in different formats, each needing a matching header, row, and footer renderer:

interface ReportComponentFactory {                 // a family of matching parts
    HeaderRenderer header();
    RowRenderer    row();
    FooterRenderer footer();
}
class PdfReportFactory implements ReportComponentFactory { ... }   // all PDF parts
class CsvReportFactory implements ReportComponentFactory { ... }   // all CSV parts

Pick PdfReportFactory and you're guaranteed a consistent set of PDF renderers - you can't accidentally mix a PDF header with a CSV footer. Use it when objects come in families that must match; it's overkill for a single object.

Builder: taming the telescoping constructor

ledger-legacy's Report object has ten fields, half optional, producing the dreaded telescoping constructor:

new Report(accountId, from, to, null, true, false, "USD", null, 2, null);  // 🚩 what do these mean?

Positional args are unreadable and error-prone (swap two booleans, silent bug). The Builder names each part and reads like prose:

Report report = Report.builder()
    .accountId("ACC-1")
    .period(from, to)
    .currency("USD")
    .includeFees(true)          // only set what you need; sensible defaults for the rest
    .build();                   // can validate invariants here before returning

The builder also gives you one place to validate invariants before the object exists, and it pairs naturally with immutability: build a fully-formed, unchangeable Report in one expression. (Lombok's @Builder or a record with a companion builder generates this for you.)

Reach for a builder past ~4 params or optional fields

A constructor with two or three required args is fine - a builder there is ceremony. The builder pays off when you have many parameters, several optional ones, or same-typed params that are easy to transpose (two Strings, two booleans). Its readability and its single validation point are the wins; don't builder-ify a two-field value object.

A parts counter and a sandwich order

A factory is the parts counter at a garage: you don't wander the warehouse grabbing components (scattered new); you tell the clerk 'brake kit for a 2019 sedan' and they hand you the right concrete part - and an abstract factory hands you a matching set (pads, rotors, and clips that fit together). A builder is a build-your-own sandwich order: instead of a cryptic combo number (positional constructor), you say 'rye, turkey, no onions, extra mustard' - each choice named, optional bits skipped, and the kitchen checks it's a valid sandwich before making it. One centralizes which object; the other clarifies how a complex one is assembled.

Choose the creational pattern

Two problems in ledger-legacy: (1) an AccountStatement object has 3 required and 6 optional fields, and call sites are full of nulls and hard-to-read positional constructors; (2) code in five places does if (region == EU) new EuTaxRules() else new UsTaxRules() to pick a tax-rules object. Which creational pattern fits each, and why not swap them?

When is the Builder pattern the right choice over a plain constructor?

Key takeaways

  • Creational patterns decouple code from concrete construction, just as Dependency Inversion decouples from concrete collaborators.
  • Factory Method centralizes the 'which concrete class' decision in one place so callers depend only on the abstraction.
  • Abstract Factory creates families of related objects that must be used together (e.g. matching PDF vs CSV report parts).
  • The Builder replaces telescoping constructors with named, optional steps, gives one place to validate invariants, and pairs with immutability.
  • Reach for a builder past ~4 params or with optional/same-typed fields; a plain constructor is better for two or three required args.
War diese Lektion hilfreich?
Diese Seite auf GitHub bearbeiten