2.2.10 · D4Design Principles

Exercises — Design Patterns — Creational - Singleton, Factory Method, Abstract Factory, Builder, Prototype

2,632 words12 min readBack to topic

Before we begin, a shared vocabulary so no word is used before it means something:

The map below is the decision rule you will apply again and again in these exercises.

Figure — Design Patterns — Creational -  Singleton, Factory Method, Abstract Factory, Builder, Prototype

L1 — Recognition

Goal: given a symptom, name the single correct pattern.

1.1 A team keeps a single application-wide ConfigRegistry; two copies would give two different truths about the same settings. Which pattern? Justify in one sentence.

Recall Solution 1.1

Singleton. The rule from the map: "want exactly 1 shared instance ⇒ Singleton". The tell-tale phrase is "two copies would give two different truths" — that is exactly the inconsistent-state problem a single global access point removes. See Singleton.

1.2 Your Dialog.render() works for any button, but which button (WinButton vs WebButton) must be chosen by whichever subclass is running. One product, subclass picks it. Which pattern?

Recall Solution 1.2

Factory Method. Symptom = one product whose concrete class is decided by a subclass overriding a creation method. Not Abstract Factory, because we make one product, not a matching family. See Factory Method.

1.3 A Windows UI must ship Windows button and Windows checkbox and Windows menu that all visually match — never a Windows button beside a Mac checkbox. Which pattern?

Recall Solution 1.3

Abstract Factory. The word "match / family / never mismatch" is the fingerprint. One factory object hands out a whole consistent kit. See Abstract Factory.

1.4 new Pizza(true, false, true, "L", false, true) — nobody can read what those booleans mean. Which pattern removes this pain?

Recall Solution 1.4

Builder. Symptom = many (often optional) constructor arguments (the "telescoping constructor" smell). Builder names each step: .addCheese().size("L"). See Builder.

1.5 Building one enemy is expensive (parses config + loads a mesh), and you need 1000 near-identical copies. Which pattern?

Recall Solution 1.5

Prototype. Symptom = "copy an existing object" because fresh construction is expensive or you only have a runtime instance to duplicate. See Prototype.


L2 — Application

Goal: actually write the pattern from first principles.

2.1 Write a thread-safe, lazy Logger Singleton using the initialization-on-demand holder idiom. Explain why it is both lazy and thread-safe.

Recall Solution 2.1
class Logger {
    private Logger() {}                 // block outside `new`
    private static class Holder {       // nested class not loaded yet...
        static final Logger I = new Logger();
    }
    public static Logger getInstance() {
        return Holder.I;                // ...loaded HERE, on first call
    }
}

Lazy: the JVM does not load Holder (and therefore does not run new Logger()) until getInstance() is first called. Thread-safe: the JVM class-loading mechanism guarantees a class is initialized exactly once, even if many threads race — no manual synchronized needed. This beats the naive if (instance == null) check, which two threads can both pass at once and each build an instance.

2.2 Turn this hard-wired code into Factory Method:

class ReportPage {
    void build() {
        Exporter e = new PdfExporter(); // hard-wired!
        e.write();
    }
}
Recall Solution 2.2

Extract the new into an overridable method:

abstract class ReportPage {
    abstract Exporter createExporter();   // the hook
    void build() {
        Exporter e = createExporter();     // shared workflow, no concrete class
        e.write();
    }
}
class PdfReportPage extends ReportPage {
    Exporter createExporter() { return new PdfExporter(); }
}
class CsvReportPage extends ReportPage {
    Exporter createExporter() { return new CsvExporter(); }
}

The shared workflow (build) stays in the base class; only the product choice varies via subclass override — exactly the point of Factory Method.

2.3 Write an immutable HttpRequest Builder with required url and optional method (default "GET") and timeout. build() must reject an empty url.

Recall Solution 2.3
final class HttpRequest {
    private final String url, method; private final int timeout;
    private HttpRequest(Builder b){ url=b.url; method=b.method; timeout=b.timeout; }
    static class Builder {
        private String url, method = "GET"; private int timeout = 30;
        Builder url(String u){ this.url = u; return this; }        // fluent: return this
        Builder method(String m){ this.method = m; return this; }
        Builder timeout(int t){ this.timeout = t; return this; }
        HttpRequest build() {
            if (url == null || url.isEmpty())
                throw new IllegalStateException("url required"); // invariant checked here
            return new HttpRequest(this);
        }
    }
}

Why return this? So calls chain: new Builder().url("x").timeout(5).build(). Why validate in build()? So no half-built or invalid object ever escapes to a client — the invariant "url is non-empty" holds for every finished object.


L3 — Analysis

Goal: find the subtle bug and say precisely why.

3.1 Count the bugs. Circle holds a mutable Point center.

class Circle implements Shape {
    Point center; int r;
    public Shape clone() {
        Circle c = new Circle();
        c.r = this.r;
        c.center = this.center;   // <-- ?
        return c;
    }
}

What breaks, and when?

Recall Solution 3.1

One bug: this is a shallow copy of center. c.center = this.center copies the reference, so the clone and the original point at the same Point object. Move the clone (c.center.x += 10) and the original moves too — silent corruption. Fix — deep copy the nested mutable field:

c.center = new Point(this.center.x, this.center.y); // fresh Point

The int r is fine to copy directly, because primitives are values, not shared references. Rule: deep-copy every mutable reference-typed field; primitives and immutables are safe to share. See Prototype.

3.2 A dev writes:

Logger a = Logger.getInstance();
Logger b = new Logger();  // compiles?

Under the Holder-idiom design of 2.1, what happens and why is that the desired behaviour?

Recall Solution 3.2

It does not compile — the constructor Logger() is private, so new Logger() is illegal outside the class. That is exactly what we want: the private constructor is the wall that makes "exactly one instance" enforceable rather than a polite request. If the constructor were public, Singleton would be a suggestion, not a guarantee.

3.3 Compare Factory Method and Abstract Factory in one crisp difference. When does adding a new product type (say Slider) hurt more?

Recall Solution 3.3

Difference: Factory Method varies one product by subclassing (override one method); Abstract Factory varies a family by composition (pass a different factory object at runtime). New product hurts Abstract Factory more: adding createSlider() to the GUIFactory interface forces every concrete factory (WinFactory, MacFactory, …) to implement it — the interface change ripples across the whole family. Factory Method only touches subclasses that care.


L4 — Synthesis

Goal: combine several patterns in one design.

4.1 Design a cross-platform game spawner. Requirements: (a) enemies + items must match a chosen theme (Forest / Lava); (b) each enemy has 12 optional traits; (c) you need 1000 near-identical orcs cheaply; (d) exactly one global GameState. Name the pattern for each requirement and give a one-line justification.

Recall Solution 4.1
  • (a) matching theme kit → Abstract Factory (ForestFactory, LavaFactory). They must stay consistent — one factory hands out both enemy and item.
  • (b) 12 optional traits → Builder for the enemy. Avoids a 12-argument telescoping constructor; each trait is a named step.
  • (c) 1000 near-identical orcs, expensive build → Prototype. Build one orc, then clone() (deep copy) the rest — skip the heavy construction 999 times.
  • (d) one GameStateSingleton. Single source of truth; a second copy would desync the game.

Notice they compose cleanly: the Abstract Factory can use a Builder internally, and the objects it returns can be Prototypes. See Abstract Factory, Builder, Prototype, Singleton.

4.2 Your Singleton GameState is making unit tests painful — every test shares the same mutable state and they interfere. What principle fixes this, and how does it change the design?

Recall Solution 4.2

Apply Dependency Injection: instead of code reaching out with GameState.getInstance(), pass a GameState object into each class's constructor. See Dependency Injection. Now a test can pass a fresh, isolated GameState, and production passes the real one. The Singleton's hidden global dependency becomes an explicit, swappable parameter — testable and honest. (Reach for raw Singleton only for truly unique resources you never need to fake.)


L5 — Mastery

Goal: reason under real-world constraints and edge cases.

5.1 You ship the Holder-idiom Singleton, but a new requirement says: "for load tests we sometimes need a second isolated Logger." Can the Holder idiom bend? What's the honest verdict and the correct move?

Recall Solution 5.1

Honest verdict: the Holder idiom cannot bend — its guarantee is "exactly one, forever," enforced by the private constructor. That guarantee is now wrong for the requirement. Do not hack a reset() back door (it reintroduces the mutable-global bug the pattern hid). Correct move: drop Singleton and switch to Dependency Injection — make Logger an ordinary class and let the container hand out one shared instance in production and a fresh one in tests. Lesson: a pattern is a tool for a constraint; when the constraint changes, change the pattern.

5.2 A Prototype clone of an object graph A → B → C (A holds B, B holds C, all mutable). A "one-level deep copy" copies A and B fresh but still shares C. Draw where the sharing lives and state the rule that makes the clone fully independent.

Recall Solution 5.2

See the figure below: the fresh copy A' gets its own B', but B' still points at the original C — so mutating C through the clone corrupts the original graph.

Figure — Design Patterns — Creational -  Singleton, Factory Method, Abstract Factory, Builder, Prototype

Rule (recursive deep copy): a clone is independent only if every mutable object reachable from it is itself freshly copied — copying must recurse all the way down the graph. Stop recursing only at immutable/primitive values (safe to share). Here you must also clone() C inside B's clone.

5.3 In the naive lazy Singleton, two threads race the instance == null check. Give the exact interleaving that produces two instances, and name the strongest fix.

Recall Solution 5.3

Interleaving:

  1. Thread T1 reads instancenull, passes the check.
  2. Before T1 runs instance = new Logger(), the OS switches to T2.
  3. Thread T2 reads instance → still null, passes the check.
  4. T2 does new Logger() (instance #1); T1 resumes and does new Logger() (instance #2).

Result: two Loggers, both handed out to different callers — the guarantee is broken. Strongest fix: the initialization-on-demand Holder idiom (2.1), because the JVM guarantees a class initializes exactly once with zero lock contention on the read path.


Recall One-line self-test

Match the symptom to the pattern ::: 1 shared → Singleton · one product, subclass picks → Factory Method · matching family → Abstract Factory · many optional args → Builder · copy existing → Prototype.


Design Principles · Factory Method · Abstract Factory · Builder · Prototype · Singleton · Dependency Injection · Structural Patterns · Behavioral Patterns