Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Java Design Patterns Bookshop Examples
# Java Design Patterns -- Bookshop Examples

📖 **Read this as a website:** https://williajm.github.io/java_patterns/
**Read this as a website:** https://williajm.github.io/java_patterns/

Mainstream design patterns you'll actually meet in real Java codebases, each demonstrated with a
small, self-contained **bookshop** example. No academic filler: only patterns that working Java
Expand All @@ -12,7 +12,7 @@ Every pattern has:
- a focused implementation under `src/main/java/com/bookshop/<category>/<pattern>/`
- a `README.md` in the same package explaining the problem, the benefits, and where the pattern
appears in the wild
- a JUnit test written as **executable documentation** read the tests to see the pattern's
- a JUnit test written as **executable documentation** -- read the tests to see the pattern's
payoff demonstrated, not just described

## The patterns
Expand All @@ -21,15 +21,15 @@ Every pattern has:
|---|---|---|---|
| Creational | [Builder](src/main/java/com/bookshop/creational/builder/README.md) | Assembling an `Order` with optional gift message, promo code, express delivery | Readable construction of immutable objects; no telescoping constructors |
| Creational | [Factory Method](src/main/java/com/bookshop/creational/factorymethod/README.md) | Creating card/PayPal/gift-card processors from a `PaymentMethod` | Callers never couple to concrete classes; new methods touch one place |
| Creational | [Singleton](src/main/java/com/bookshop/creational/singleton/README.md) | Shop-wide config and feature flags via the enum idiom | Guaranteed single shared instance with an honest "prefer DI" caveat |
| Creational | [Singleton](src/main/java/com/bookshop/creational/singleton/README.md) | Shop-wide config and feature flags via the enum idiom | Guaranteed single shared instance -- with an honest "prefer DI" caveat |
| Structural | [Adapter](src/main/java/com/bookshop/structural/adapter/README.md) | Wrapping a legacy pence-and-status-codes payment gateway behind a clean interface | Incompatible APIs cooperate; the ugliness is quarantined in one class |
| Structural | [Decorator](src/main/java/com/bookshop/structural/decorator/README.md) | Stacking gift wrap, express handling and greeting card onto an order | Combine extras at runtime without 2ⁿ subclasses |
| Structural | [Decorator](src/main/java/com/bookshop/structural/decorator/README.md) | Stacking gift wrap, express handling and greeting card onto an order | Combine extras at runtime without 2^n subclasses |
| Structural | [Facade](src/main/java/com/bookshop/structural/facade/README.md) | One `checkout()` call orchestrating inventory, payment and shipping | The correct sequence is written once; callers can't get it wrong |
| Structural | [Proxy](src/main/java/com/bookshop/structural/proxy/README.md) | A caching stand-in for the distributor's slow remote catalog | Caching/laziness/access control added without touching callers |
| Behavioral | [Strategy](src/main/java/com/bookshop/behavioral/strategy/README.md) | Swappable discount rules: loyalty, bulk, seasonal sale | Pricing rules are configuration, not `if/else` ladders; each testable alone |
| Behavioral | [Observer](src/main/java/com/bookshop/behavioral/observer/README.md) | Email/SMS/dashboard reactions to restocks | Event source is decoupled from its ever-growing list of reactions |
| Behavioral | [Template Method](src/main/java/com/bookshop/behavioral/templatemethod/README.md) | One sales-report skeleton, CSV and HTML variants | The algorithm exists once; formats can't drift apart |
| Behavioral | [Chain of Responsibility](src/main/java/com/bookshop/behavioral/chainofresponsibility/README.md) | Order validation pipeline: stock address fraud | Checks are independent components; the pipeline is just a list |
| Behavioral | [Chain of Responsibility](src/main/java/com/bookshop/behavioral/chainofresponsibility/README.md) | Order validation pipeline: stock -> address -> fraud | Checks are independent components; the pipeline is just a list |

## Principles: SOLID + DRY

Expand All @@ -50,10 +50,10 @@ payoff.

Two docs that keep the rest of the repo honest:

- **[Anti-patterns](docs/anti-patterns.md)** the five failure modes you'll actually meet (God
- **[Anti-patterns](docs/anti-patterns.md)** -- the five failure modes you'll actually meet (God
Class, magic numbers, copy-paste, global mutable state, Golden Hammer), each with its
counter-example in this repo.
- **[Use with judgement](docs/use-with-judgement.md)** none of these are rigid rules. Every
- **[Use with judgement](docs/use-with-judgement.md)** -- none of these are rigid rules. Every
pattern and principle here has a cost as well as a benefit, overuse is its own anti-pattern, and
the page gives concrete signals for both over- and under-engineering.

Expand All @@ -65,21 +65,21 @@ Requires JDK 17+ and Maven.
mvn verify # lints (Checkstyle) and runs every pattern's tests
```

## What's deliberately left out and why
## What's deliberately left out -- and why

Half the Gang of Four catalogue rarely appears in modern application code. Excluded on purpose:

- **Command** its niche (tasks as objects) is covered by `Runnable` and lambdas; hand-rolled
- **Command** -- its niche (tasks as objects) is covered by `Runnable` and lambdas; hand-rolled
undo stacks are rare.
- **State** real projects usually reach for an enum + switch or a state-machine library rather
- **State** -- real projects usually reach for an enum + switch or a state-machine library rather
than the class-per-state shape.
- **Composite** common *inside* UI/tree libraries, rarely hand-written in application code.
- **Iterator** absorbed into the language: `Iterable` and for-each.
- **Abstract Factory, Prototype, Bridge, Flyweight, Visitor, Mediator, Memento, Interpreter**
- **Composite** -- common *inside* UI/tree libraries, rarely hand-written in application code.
- **Iterator** -- absorbed into the language: `Iterable` and for-each.
- **Abstract Factory, Prototype, Bridge, Flyweight, Visitor, Mediator, Memento, Interpreter** --
superseded by dependency injection, enums and lambdas, or simply too rare to earn a place in a
"patterns you'll actually use" list.

If you're deciding whether to use a pattern at all: the tests here show the *problem* each pattern
earns its keep on. No matching problem, no pattern the simplest code that works wins. The full
earns its keep on. No matching problem, no pattern -- the simplest code that works wins. The full
argument, including the signals of over- and under-engineering, is in
[Use with judgement](docs/use-with-judgement.md).
2 changes: 1 addition & 1 deletion _config.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
title: Java Design Patterns Bookshop Examples
title: Java Design Patterns -- Bookshop Examples
description: >-
Mainstream Java design patterns and SOLID principles, each demonstrated with a
small bookshop example and tests written as executable documentation.
Expand Down
18 changes: 9 additions & 9 deletions docs/anti-patterns.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Anti-patterns

The failure modes you'll actually meet. These are shown as snippets only deliberately bad code
The failure modes you'll actually meet. These are shown as snippets only -- deliberately bad code
doesn't belong in `src/` where it might get copied. Each entry links to the counter-example
elsewhere in this repo.

None of these start as sabotage. Each one is a *reasonable shortcut taken one time too many*
None of these start as sabotage. Each one is a *reasonable shortcut taken one time too many* --
which is why they're worth naming: you recognise the slide while it's still cheap to stop.

## 1. God Class (the Blob)
Expand All @@ -20,7 +20,7 @@ class BookshopManager {
**How it happens:** every new feature is "just one more method" on the class that already has the
data. **The damage:** every change risks everything else; nobody can test (or safely delete)
anything; merge conflicts concentrate in one file. **The counter:** one reason to change per class
[SRP](../src/main/java/com/bookshop/solid/srp/README.md) and a
-- [SRP](../src/main/java/com/bookshop/solid/srp/README.md) -- and a
[Facade](../src/main/java/com/bookshop/structural/facade/README.md) when callers genuinely want
one entry point over many small parts.

Expand All @@ -32,10 +32,10 @@ if (status == 3) { refund(order); } // 3 means.
```

**How it happens:** the value was obvious to whoever typed it, that afternoon. **The damage:** the
business rule is unfindable and unsearchable; when "free shipping over £25" becomes £30, some of
business rule is unfindable and unsearchable; when "free shipping over GBP 25" becomes GBP 30, some of
the 25.00s get updated (see [copy-paste programming](#3-copy-paste-programming)); `status == 3`
invites off-by-one bugs no compiler can catch. **The counter:** named constants
(`FREE_SHIPPING_THRESHOLD`) and enums
(`FREE_SHIPPING_THRESHOLD`) and enums --
[`PaymentMethod`](../src/main/java/com/bookshop/creational/factorymethod/PaymentMethod.java) makes
invalid payment types unrepresentable, and the compiler checks every `switch` over it.

Expand All @@ -47,10 +47,10 @@ class SupplierInvoice { ... net.multiply(new BigDecimal("1.20")) ... }
class RefundNote { ... net.multiply(new BigDecimal("1.175")) ... } // missed in the update
```

**How it happens:** copying working code *feels* safe it worked over there. **The damage:** one
**How it happens:** copying working code *feels* safe -- it worked over there. **The damage:** one
piece of knowledge now lives in several places and they drift; every copy must be found for every
change, and the one you miss is a silent bug. **The counter:**
[DRY](../src/main/java/com/bookshop/dry/README.md) one authoritative home per piece of
[DRY](../src/main/java/com/bookshop/dry/README.md) -- one authoritative home per piece of
knowledge. (And note DRY's own overuse warning there: deduplicate knowledge, not coincidence.)

## 4. Global mutable state (Singleton abuse)
Expand Down Expand Up @@ -79,9 +79,9 @@ public class AbstractBookFactoryStrategyProviderImpl
implements BookFactoryStrategyProvider { // it returns... a Book. That's all it does.
```

**How it happens:** patterns get learned, then get *looked for* every problem starts resembling
**How it happens:** patterns get learned, then get *looked for* -- every problem starts resembling
the new hammer. This is the anti-pattern of this very repo's subject matter. **The damage:** five
files and three indirections where a constructor call belonged; readers must unwind ceremony to
find two lines of logic; the codebase optimises for imagined futures over the present. **The
counter:** every pattern here opens with *the problem it solves*. No matching problem, no pattern
the full argument is on [Use with judgement](use-with-judgement.md).
-- the full argument is on [Use with judgement](use-with-judgement.md).
18 changes: 9 additions & 9 deletions docs/use-with-judgement.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Use with judgement

Everything in this repo the patterns, SOLID, DRY is a **tool for a specific problem**, not a
Everything in this repo -- the patterns, SOLID, DRY -- is a **tool for a specific problem**, not a
rule to comply with. That framing matters, because every one of these tools has a cost, and paying
the cost without having the problem makes code *worse*. Design maturity isn't knowing the
patterns; it's knowing when not to use them.
Expand All @@ -11,20 +11,20 @@ The benefit column in this repo's tables is real, but so is the invoice:

| Tool | Solves | Costs when the problem isn't there |
|---|---|---|
| Builder | Many optional fields | Fifty lines of ceremony for a two-field class a constructor was clearer |
| Builder | Many optional fields | Fifty lines of ceremony for a two-field class -- a constructor was clearer |
| Factory Method | Callers coupled to concrete types | An indirection layer over a single `new` that never varies |
| Strategy | Multiple interchangeable algorithms | An interface, a context, and one lonely implementation an `if` was clearer |
| Strategy | Multiple interchangeable algorithms | An interface, a context, and one lonely implementation -- an `if` was clearer |
| Interfaces everywhere | Multiple implementations / substitution in tests | `BookService` + `BookServiceImpl` pairs that only ever have one member |
| SRP | Unrelated reasons-to-change tangled together | Logic shattered into confetti ten two-line classes nobody can follow |
| SRP | Unrelated reasons-to-change tangled together | Logic shattered into confetti -- ten two-line classes nobody can follow |
| DRY | One piece of knowledge in several places | Coincidentally-similar code welded together, coupling rules that change separately |
| Observer | Many independent reactions to an event | Control flow nobody can trace "what happens when I restock?" has no findable answer |
| Observer | Many independent reactions to an event | Control flow nobody can trace -- "what happens when I restock?" has no findable answer |

## Signals you're overdoing it

- More structure than logic: files, interfaces and indirections outnumber the lines that actually
do something.
- Abstractions with exactly one caller or one implementation, kept "because we might need it".
You aren't gonna need it (YAGNI) and when you are, *that's* the day to add it.
You aren't gonna need it (YAGNI) -- and when you are, *that's* the day to add it.
- You can't answer "what problem does this layer solve?" with something that has already happened
at least once.
- Reading a two-line change requires opening five files.
Expand All @@ -34,20 +34,20 @@ The benefit column in this repo's tables is real, but so is the invoice:
- Shotgun surgery: one business change means edits scattered across many files (missing DRY/SRP).
- An `if/else` ladder that grows every sprint (Strategy/OCP's problem has arrived).
- Business logic you can't test without credentials, networks, or a database (DIP's problem).
- A class everyone edits and nobody understands (the God Class see
- A class everyone edits and nobody understands (the God Class -- see
[anti-patterns](anti-patterns.md)).

## How to calibrate

1. **Start with the simplest thing that works.** A constructor, a plain method, one class.
2. **Let the problem show up before the solution.** The *second or third* time a change hurts, the
real shape of the abstraction is visible extract it then (the "rule of three"). Patterns
real shape of the abstraction is visible -- extract it then (the "rule of three"). Patterns
applied speculatively usually guess the wrong axis of change.
3. **Refactor *toward* patterns, don't start *from* them.** Their biggest everyday value is
vocabulary: "this wants to be a Strategy" is a one-sentence design review.
4. **Match the effort to the mattering.** The order-money path deserves DIP and careful seams; a
one-off report script does not.

The tests in this repo model this deliberately: each one demonstrates a pattern *earning its keep*
against a concrete problem telescoping constructors, drifting VAT copies, an untestable payment
against a concrete problem -- telescoping constructors, drifting VAT copies, an untestable payment
call. If your code doesn't have the problem, it shouldn't have the pattern.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Java Design Patterns Bookshop Examples</name>
<name>Java Design Patterns -- Bookshop Examples</name>
<description>Mainstream Java design patterns demonstrated with bookshop examples.</description>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Before the shop accepts an order it must pass a series of checks: is everything in stock? is there
a shipping address? does it look fraudulent? A single `validateOrder` method holding all the rules
becomes a wall of `if`s where the rules can't be reordered, reused, or tested independently and
becomes a wall of `if`s where the rules can't be reordered, reused, or tested independently -- and
every new rule means editing (and re-risking) the same method.

## The pattern
Expand All @@ -23,11 +23,11 @@ This is the servlet-filter / middleware shape: the pipeline is *configuration*,

## Benefits

- **Steps are independent** each check is a small class with one job, tested on its own.
- **The pipeline is data** add, remove, or reorder checks by editing the list, not the logic.
- **Short-circuiting for free** a rejection stops the chain; later (possibly expensive) checks
- **Steps are independent** -- each check is a small class with one job, tested on its own.
- **The pipeline is data** -- add, remove, or reorder checks by editing the list, not the logic.
- **Short-circuiting for free** -- a rejection stops the chain; later (possibly expensive) checks
never run.
- **Separation of *what checks exist* from *which run*** different channels (web, phone, trade)
- **Separation of *what checks exist* from *which run*** -- different channels (web, phone, trade)
can assemble different chains from the same parts.

## Seen in the wild
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

/**
* The subject: knows only that listeners exist, never what they do. Email, SMS,
* dashboards all the same to the inventory.
* dashboards -- all the same to the inventory.
*/
public class Inventory {

Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/bookshop/behavioral/observer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
When a sold-out book is restocked, several things should happen: waitlisted customers get an email,
the storefront's "notify me" subscribers get a text, the sales dashboard updates. If `Inventory`
calls the email service, the SMS service, and the dashboard directly, the stock system depends on
every notification channel and grows a new dependency each time marketing adds one.
every notification channel -- and grows a new dependency each time marketing adds one.

## The pattern

Expand All @@ -20,11 +20,11 @@ inventory.restock(book); // every subscriber is notified

## Benefits

- **Loose coupling** the subject knows the listener *interface* only; notification channels come
- **Loose coupling** -- the subject knows the listener *interface* only; notification channels come
and go without touching inventory code.
- **Open-ended reactions** new behaviour (push notifications, analytics) is a new subscriber, not
- **Open-ended reactions** -- new behaviour (push notifications, analytics) is a new subscriber, not
a change to the event source.
- **Dynamic at runtime** subscribe and unsubscribe as customers opt in and out.
- **Dynamic at runtime** -- subscribe and unsubscribe as customers opt in and out.

## A note on modern practice

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

/**
* The strategy: one interchangeable pricing rule. A functional interface, so
* one-off strategies can be lambdas the modern face of this pattern.
* one-off strategies can be lambdas -- the modern face of this pattern.
*/
@FunctionalInterface
public interface DiscountStrategy {
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/com/bookshop/behavioral/strategy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ PriceCalculator januarySale = new PriceCalculator(Discounts.seasonalSale(20));
PriceCalculator everyday = new PriceCalculator(Discounts.loyalty());
```

`DiscountStrategy` is a `@FunctionalInterface`, so a one-off promotion is just a lambda which is
`DiscountStrategy` is a `@FunctionalInterface`, so a one-off promotion is just a lambda -- which is
how the pattern usually appears in modern Java (`Comparator` being the canonical example).

## Benefits

- **Swap algorithms at runtime** the active promotion is data/configuration, not code structure.
- **Each rule tested in isolation** a strategy is a tiny pure function; no combinatorial
- **Swap algorithms at runtime** -- the active promotion is data/configuration, not code structure.
- **Each rule tested in isolation** -- a strategy is a tiny pure function; no combinatorial
calculator tests.
- **Open/closed** a new promotion is a new strategy; the calculator and existing strategies are
- **Open/closed** -- a new promotion is a new strategy; the calculator and existing strategies are
untouched.
- **No conditional ladders** the dispatch is polymorphism, not `if (promoType == ...)`.
- **No conditional ladders** -- the dispatch is polymorphism, not `if (promoType == ...)`.

## Seen in the wild

Expand Down
Loading
Loading