Refactoring Toward Creational Patterns — Hands-On Tasks¶
Source: Joshua Kerievsky, Refactoring to Patterns (Addison-Wesley, 2004); refactoring.guru/design-patterns/creational-patterns
Each task gives smelly creation code, names the refactoring to apply, and asks for the mechanical steps + the after-code. Work it yourself, then expand the solution. Keep every step behavior-preserving — the resulting objects must be identical.
Task 1 — Replace Constructors with Creation Methods¶
Smelly code:
public class Money {
private final long amount; // minor units (cents)
private final String currency;
public Money(long amount, String currency) { this.amount = amount; this.currency = currency; }
public Money(double majorUnits, String currency) { // dollars → cents
this(Math.round(majorUnits * 100), currency);
}
}
new Money(5, "USD") — is that 5 cents or $5? 5 is an int, widens to long, so it silently calls the cents constructor. Ambiguous and bug-prone. Refactor to named creation methods.
Worked solution
Steps: (1) add `Money.ofCents(long, String)` and `Money.ofMajor(double, String)` delegating to a single private constructor; (2) redirect callers; (3) make constructors private.public class Money {
private final long amount;
private final String currency;
private Money(long amount, String currency) { this.amount = amount; this.currency = currency; }
public static Money ofCents(long cents, String currency) { return new Money(cents, currency); }
public static Money ofMajor(double major, String currency) {
return new Money(Math.round(major * 100), currency);
}
}
Money fiveDollars = Money.ofMajor(5, "USD"); // unambiguous
Money fiveCents = Money.ofCents(5, "USD");
Task 2 — Encapsulate Classes with Factory¶
Smelly code:
public interface Exporter { byte[] export(Report r); }
public class PdfExporter implements Exporter { public byte[] export(Report r){ /*...*/ } }
public class CsvExporter implements Exporter { public byte[] export(Report r){ /*...*/ } }
public class HtmlExporter implements Exporter { public byte[] export(Report r){ /*...*/ } }
// Duplicated across three call sites:
Exporter e;
if (fmt.equals("pdf")) e = new PdfExporter();
else if (fmt.equals("csv")) e = new CsvExporter();
else e = new HtmlExporter();
Hide the concrete exporters behind a factory; clients depend only on Exporter.
Worked solution
Steps: (1) create `ExporterFactory` with `create(Format)`; (2) move the branching in; (3) redirect all three call sites; (4) make the concrete classes package-private; (5) enum discriminator for type safety.public final class ExporterFactory {
public enum Format { PDF, CSV, HTML }
public Exporter create(Format f) {
return switch (f) {
case PDF -> new PdfExporter();
case CSV -> new CsvExporter();
case HTML -> new HtmlExporter();
};
}
}
// PdfExporter/CsvExporter/HtmlExporter drop `public` → package-private.
// Clients: exporterFactory.create(Format.PDF) — concretions are now a package secret.
Task 3 — Introduce Polymorphic Creation with Factory Method¶
Smelly code:
abstract class ConnectionPoolManager {
abstract void warmUp();
}
class PostgresPoolManager extends ConnectionPoolManager {
void warmUp() {
// 15 identical lines of pool-sizing logic ...
DataSource ds = new PgDataSource(); // only difference
// 10 identical lines registering ds ...
}
}
class MySqlPoolManager extends ConnectionPoolManager {
void warmUp() {
// the SAME 15 lines ...
DataSource ds = new MySqlDataSource(); // only difference
// the SAME 10 lines ...
}
}
Deduplicate via a Factory Method.
Worked solution
Steps: (1) extract `createDataSource()` in each subclass; (2) give them the same signature; (3) pull the duplicated `warmUp()` body up to the superclass, calling `createDataSource()`; (4) make `createDataSource()` abstract; (5) delete the empty `warmUp()` overrides.abstract class ConnectionPoolManager {
final void warmUp() {
// 15 lines of pool-sizing (ONCE) ...
DataSource ds = createDataSource(); // polymorphic hook
// 10 lines registering ds (ONCE) ...
}
protected abstract DataSource createDataSource(); // Factory Method
}
class PostgresPoolManager extends ConnectionPoolManager {
protected DataSource createDataSource() { return new PgDataSource(); }
}
class MySqlPoolManager extends ConnectionPoolManager {
protected DataSource createDataSource() { return new MySqlDataSource(); }
}
Task 4 — Replace telescoping constructors with a Builder¶
Smelly code:
public class EmailMessage {
public EmailMessage(String to) { this(to, null); }
public EmailMessage(String to, String subject) { this(to, subject, null); }
public EmailMessage(String to, String subject, String body) { this(to, subject, body, false); }
public EmailMessage(String to, String subject, String body, boolean html) {
this(to, subject, body, html, List.of());
}
public EmailMessage(String to, String subject, String body, boolean html, List<String> cc) { /*...*/ }
}
// new EmailMessage("a@x.com", "Hi", "Body", true, List.of("b@x.com")) — opaque
Introduce a Builder. to is required; the rest optional. build() must reject an empty to.
Worked solution
Steps: (1) static nested `Builder`, one field per param; (2) required `to` in the Builder constructor, fluent setters for the rest; (3) `build()` validates then calls the private constructor; (4) private constructor; (5) migrate callers; (6) delete telescoping constructors.public class EmailMessage {
private final String to, subject, body; private final boolean html; private final List<String> cc;
private EmailMessage(Builder b){ to=b.to; subject=b.subject; body=b.body; html=b.html; cc=b.cc; }
public static class Builder {
private final String to;
private String subject = ""; private String body = "";
private boolean html = false; private List<String> cc = List.of();
public Builder(String to){ this.to = to; }
public Builder subject(String s){ this.subject = s; return this; }
public Builder body(String b){ this.body = b; return this; }
public Builder html(boolean h){ this.html = h; return this; }
public Builder cc(List<String> c){ this.cc = List.copyOf(c); return this; }
public EmailMessage build() {
if (to == null || to.isBlank()) throw new IllegalStateException("recipient required");
return new EmailMessage(this);
}
}
}
EmailMessage m = new EmailMessage.Builder("a@x.com").subject("Hi").body("Body").html(true).build();
Task 5 — Move Creation Knowledge to Factory¶
Smelly code:
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(String provider, Map<String,String> secrets) {
if (provider.equals("stripe")) gateway = new StripeGateway(secrets.get("stripe_key"));
else if (provider.equals("paypal")) gateway = new PaypalGateway(secrets.get("pp_id"), secrets.get("pp_secret"));
else gateway = new MockGateway();
}
void charge(Order o) { gateway.charge(o.total()); }
}
CheckoutService knows how to build gateways — not its job. Move it out.
Worked solution
Steps: (1) `PaymentGatewayFactory` with `create(provider, secrets)`; (2) move branching in; (3) `CheckoutService` receives a ready `PaymentGateway`; (4) caller obtains gateway from factory, injects it.class PaymentGatewayFactory {
PaymentGateway create(String provider, Map<String,String> secrets) {
return switch (provider) {
case "stripe" -> new StripeGateway(secrets.get("stripe_key"));
case "paypal" -> new PaypalGateway(secrets.get("pp_id"), secrets.get("pp_secret"));
default -> new MockGateway();
};
}
}
class CheckoutService {
private final PaymentGateway gateway;
CheckoutService(PaymentGateway gateway) { this.gateway = gateway; } // just receives it
void charge(Order o) { gateway.charge(o.total()); }
}
Task 6 — Extract Factory from a bloated class¶
Smelly code:
class IngestionPipeline {
void run(Path file) { /* real ingestion logic */ }
// creation responsibilities that crept in:
Parser newParser(String ext) { return ext.equals("csv") ? new CsvParser() : new JsonParser(); }
Validator newValidator(Schema s) { return new SchemaValidator(s); }
Sink newSink(Config c) { return c.dryRun() ? new NullSink() : new DbSink(c.dsn()); }
}
Lift the creation cluster into a factory; restore single responsibility.
Worked solution
Steps: (1) `IngestionComponentFactory`; (2) Move Method for each of the three (update call sites, run tests each time); (3) inject the factory into `IngestionPipeline`; (4) confirm the pipeline holds no creation methods.class IngestionComponentFactory {
Parser newParser(String ext) { return ext.equals("csv") ? new CsvParser() : new JsonParser(); }
Validator newValidator(Schema s) { return new SchemaValidator(s); }
Sink newSink(Config c) { return c.dryRun() ? new NullSink() : new DbSink(c.dsn()); }
}
class IngestionPipeline {
private final IngestionComponentFactory factory;
IngestionPipeline(IngestionComponentFactory factory) { this.factory = factory; }
void run(Path file) { /* ingestion only, asks factory for components */ }
}
Task 7 — Refactor toward Abstract Factory¶
Smelly code:
class ThemeFactory {
Button createButton(boolean dark) { return dark ? new DarkButton() : new LightButton(); }
Panel createPanel(boolean dark) { return dark ? new DarkPanel() : new LightPanel(); }
Icon createIcon(boolean dark) { return dark ? new DarkIcon() : new LightIcon(); }
}
The dark discriminator is duplicated three times and lets callers mix a DarkButton with a LightPanel. Refactor to Abstract Factory.
Worked solution
Steps: (1) extract an interface `ThemeFactory { Button createButton(); Panel createPanel(); Icon createIcon(); }` (no discriminator); (2) one impl per value — `DarkThemeFactory`, `LightThemeFactory`; (3) move the `dark ? ... : ...` decision to the composition root (`dark ? new DarkThemeFactory() : new LightThemeFactory()`); (4) inject the chosen factory; (5) delete the old class.interface ThemeFactory { Button createButton(); Panel createPanel(); Icon createIcon(); }
class DarkThemeFactory implements ThemeFactory {
public Button createButton(){ return new DarkButton(); }
public Panel createPanel(){ return new DarkPanel(); }
public Icon createIcon(){ return new DarkIcon(); }
}
class LightThemeFactory implements ThemeFactory { /* light family */ }
// Root: ThemeFactory tf = dark ? new DarkThemeFactory() : new LightThemeFactory();
Task 8 — Refactor toward Prototype¶
Smelly code:
class SimulationRunner {
Grid run(String scenario) {
Grid g = new Grid(parseTerrain("map.dat"), computeDefaultRules()); // expensive: ~400ms each
applyScenario(g, scenario);
return g;
}
}
// Called 500×; the 400ms construction repeats every time.
The terrain parse + default-rules computation is identical across calls. Refactor to Prototype.
Worked solution
Steps: (1) build the baseline `Grid` once and hold it as a prototype; (2) add a copy constructor (deep-copy mutable rules, share immutable terrain); (3) replace per-call construction with `prototype.copy()` + scenario; (4) verify a clone's mutation can't leak into the prototype.class SimulationRunner {
private final Grid prototype = new Grid(parseTerrain("map.dat"), computeDefaultRules()); // once
Grid run(String scenario) {
Grid g = prototype.copy(); // cheap
applyScenario(g, scenario);
return g;
}
}
final class Grid {
private final Terrain terrain; // immutable → share
private Ruleset rules; // mutable → deep-copy
Grid(Terrain t, Ruleset r){ terrain=t; rules=r; }
Grid(Grid o){ this.terrain = o.terrain; this.rules = o.rules.copy(); }
Grid copy(){ return new Grid(this); }
}
Task 9 — Inline Singleton¶
Smelly code:
class FeatureFlags {
private static final FeatureFlags INSTANCE = new FeatureFlags();
private final Map<String,Boolean> flags = loadFromEnv();
private FeatureFlags() {}
public static FeatureFlags getInstance() { return INSTANCE; }
public boolean isOn(String key) { return flags.getOrDefault(key, false); }
}
class Renderer {
void draw() { if (FeatureFlags.getInstance().isOn("new_ui")) drawNewUi(); }
}
Tests can't override flags; the dependency is hidden. Inline the Singleton.
Worked solution
Steps: (1) create one `FeatureFlags` at the composition root; (2) give `Renderer` a `FeatureFlags` field via constructor; (3) replace `getInstance()` with the field; (4) wire from the root; (5) delete the static instance + `getInstance()`, open the constructor.class FeatureFlags {
private final Map<String,Boolean> flags;
public FeatureFlags(Map<String,Boolean> flags) { this.flags = flags; } // injectable
public boolean isOn(String key) { return flags.getOrDefault(key, false); }
}
class Renderer {
private final FeatureFlags flags;
Renderer(FeatureFlags flags) { this.flags = flags; } // explicit dependency
void draw() { if (flags.isOn("new_ui")) drawNewUi(); }
}
// Root: FeatureFlags flags = new FeatureFlags(loadFromEnv()); new Renderer(flags);
Next¶
- find-bug.md — diagnose broken/mis-factored creation code.
- optimize.md — propose (or reject) creational refactorings.
- Back to theory: junior.md · middle.md · senior.md · professional.md
In this topic