Java Design Patterns

Last update: 2022-08-29 23:50:54 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

Patterns name recurring design trade-offs. Java language features can implement a pattern, but neither a lambda nor a dependency-injection container is itself a replacement for a design decision.

Creational Patterns #

A Factory centralizes construction when callers must not depend on a concrete type or construction selects an implementation. A factory hides useful details, but a factory with a method for every product can become an unnecessary indirection.

java
interface Parser { Document parse(String text); }
final class Parsers {
    static Parser forFormat(Format format) { return format::parse; }
}

A Builder collects optional arguments and validates cross-field invariants before producing an immutable value. Use a constructor or static factory when there are few mandatory fields; a builder that only mirrors setters obscures the required state. A Prototype is often clearer in Java as an explicit copy constructor or copyOf factory than Cloneable, whose shallow-copy and inheritance semantics are difficult to extend safely.

Structural Patterns #

An Adapter translates an external interface into an application-owned contract. It prevents vendor types from spreading through the domain layer, at the cost of maintaining the translation. A Decorator adds behavior around the same interface:

java
final class MeteredStore implements Store {
    private final Store delegate;
    public Value get(Key key) {
        metrics.recordAttempt();
        return delegate.get(key);
    }
}

Decorator composition is preferable to subclass combinations when concerns such as caching, authorization, and metrics vary independently. Java I/O streams are a classic example. Facade and composite are similarly useful when they simplify an intentionally complex subsystem or model part-whole trees.

Behavioral Patterns #

Strategy selects an algorithm behind a stable interface. Java expresses it with an interface implementation, a lambda for a functional interface, or enum constants for a closed strategy set:

java
enum Rounding {
    DOWN { BigDecimal apply(BigDecimal n) { return n.setScale(0, RoundingMode.DOWN); } },
    UP   { BigDecimal apply(BigDecimal n) { return n.setScale(0, RoundingMode.UP); } };
    abstract BigDecimal apply(BigDecimal n);
}

Observer publishes events to listeners. Specify synchronous versus asynchronous delivery, ordering, failure isolation, backpressure, and listener ownership; otherwise an observer list becomes hidden control flow. Command, template method, iterator, and state patterns remain useful when each makes variation explicit without a large conditional.

Dependency injection supplies collaborators from outside an object. It can wire factories, strategies, decorators, and observers, but it does not choose their boundaries. Likewise, records, sealed types, switch, and lambdas reduce boilerplate; they do not decide whether open-ended polymorphism or a closed set models the domain.

Comments

Read or comment on GitHub (sign-in required to comment)