Chapter Roadmap (1 of 3)
- 8.1 When does creating an object become a design problem?
- Creation, compatibility and added behaviour are different problems.
- 8.2 Factory Method: letting subclasses choose what to create
- An overridable creation hook inside an inherited workflow.
- 8.3 Abstract Factory: products that must belong together
- Related product roles that must vary together.
- 8.4 Builder: configuring a value you can trust
- Configure, validate, publish an immutable value.
Chapter Roadmap (2 of 3)
- 8.5 Adapter: translating an outside system into your own terms
- The one pattern wired into the project: a directory adapter.
- 8.6 Decorator: adding behaviour around an operation
- Optional behaviour around an operation; order and failure matter.
- 8.7 When the simpler option is the better one
- A decision another developer can challenge.
- 8.8 Classifying an AI’s pattern suggestions before accepting them classify before accepting.
Chapter Roadmap (3 of 3)
- 8.9 Pattern habits that mislead
- 8.10 Try it yourself: the directory boundary and pattern exercises
- Nine steps; step 9 is on your team’s project.
- 8.11 Questions to think with, and a transfer challenge
- A library application with numeric category codes.
- What to take away, and what comes next
- Chapter 9: changing behaviour with behavioural patterns.
Learning Outcomes (1 of 2)
By the end of this chapter, you should be able to:
- Identify a concrete construction or interface-compatibility problem.
- Distinguish Factory Method, a simple factory and Abstract Factory by their responsibilities.
- Build a validated immutable request through a staged construction interface.
Module Connection and Prerequisites
- Supports LO2. The midterm remains on Chapters 2 to 7: these patterns are not added to that scope.
- Prerequisites: Chapters 5 to 7: contracts, interfaces, constructor injection, composition, meaningful tests. Java 17 and the Chapter 7 Maven/JUnit setup.
- Readings: the Factory and Adapter chapters of Martin, and the creational and structural patterns in Gamma et al.
- All provider records are synthetic. No real directory, network service or personal data.
How to Use This Chapter
- Core reading: 8.1, 8.2, 8.5 and 8.7. Factory Method and Adapter are the two suggested live demonstrations.
- Working-knowledge reading: 8.3 (Abstract Factory), 8.4 (Builder), 8.6 (Decorator). Read each, run its example and, for the decorator, predict both audit histories before you run the tests. Working knowledge is not optional, but asks for understanding, not independent implementation.
- Minimum lab: steps 1 to 3, 5 and 9. This is midterm week: do the minimum lab first.
- If you want more: steps 4 and 6 to 8.
Two Proposals, Neither Argued
- Proposal 1: put these details inside
ReservationCoordinator. It spreads external conventions into reservation behaviour. - Proposal 2: a factory, builder, adapter and decorator around every class. It introduces structures before identifying what must vary.
- The useful question: which responsibility needs a boundary, and what is the smallest design that makes that boundary clear?
- The answer adds one justified Adapter to the running project.
Start With the Ordinary Constructor
- Creating a
LoanPeriodwith two required dates is clear: the constructor validates a small invariant. - Replacing it with a hierarchy of factories would not improve the rule.
- Chapter 6’s
ReservationApplication.create(sender)is a useful assembly function: it connects known policies, a formatter and a supplied sender.
When Construction Becomes Troublesome
- The decision about which object, which compatible group, or which configuration is repeated across places that should not own it.
- A caller needs only an export operation yet inspects an output-format flag each time.
- Another picks a heading renderer and a body renderer independently and combines incompatible formats.
- A third passes six positional arguments whose meanings are hard to review.
Creation, Compatibility, Added Behaviour
| Pattern | Concerned with |
|---|---|
| Factory Method, Abstract Factory, Builder | Construction decisions |
| Adapter | Connecting an existing interface to the one a client needs |
| Decorator | Attaching an optional responsibility while keeping the component interface |
- These categories organise discussion. Actual code must still be examined for intent and consequences.
The Directory Request Is a Translation Problem
- The awkwardness is not that constructing a directory object needs many arguments.
- It is that its data and failure meanings differ from the application’s.
- A builder would not answer what
Tmeans or whether an outage is a missing borrower. Those are translation decisions. - Similarly, an audit wrapper around a sender adds behaviour without choosing the sender’s class. Calling it a factory distracts from call order and errors.
State the Change Pressure Before the Structure
A useful design note names:
- the current simple solution,
- the change it handles poorly,
- the selected boundary,
- the cost of adding it.
- Here: passing a trusted category worked for local examples; provider codes now need central translation; an application-owned directory interface isolates it; additional types and mapping tests are the cost.
Do Not Predict Every Future Directory
- There is already one incompatible interface.
- Inventing five hypothetical provider families is not evidence that an Abstract Factory is needed today.
- Keep construction visible at the composition point: hiding it in a global registry can make dependencies harder to discover.
- The goal is understandable ownership, not a ban on constructors.
A Simple Factory Is Often Enough
- The isolated export example: validate an input, obtain an exporter, execute it.
- A simple factory can choose a plain-text or HTML exporter from a flag. That is often enough when one assembly location owns the choice.
- The Factory Method scenario assumes subclasses must specialise product creation while keeping the workflow inherited from a common creator.
- The pressure is an extension point in the workflow, not merely two product classes.
The Structure
public abstract static class ExportJob {
protected abstract Exporter createExporter();
public final String run(String value) {
Objects.requireNonNull(value, "value");
return createExporter().export(value);
}
}runowns the workflow and isfinal. The protected hook is the deliberate variation point.
The Two Creators
public static final class PlainJob extends ExportJob {
@Override protected Exporter createExporter() { return value -> "TEXT: " + value; }
}
public static final class HtmlJob extends ExportJob {
@Override protected Exporter createExporter() { return value -> "<p>" + escape(value) + "</p>"; }
}- The products are lambdas in this compact example. They still implement the product interface.
Reading the Notation
#marks a protected member: subclasses can overridecreateExporter, outside callers cannot call it.<<abstract>>marks a class that cannot be instantiated on its own.- A solid line with a hollow triangle is inheritance.
- The dashed arrow is a dependency:
ExportJobcreates and uses exporters without keeping one in a field. - It is Factory Method because subclasses vary creation inside an inherited workflow.
Compare With a Simple Factory Honestly
public static Exporter simpleExporter(boolean html) {
return html ? value -> "<p>" + escape(value) + "</p>"
: value -> "TEXT: " + value;
}- An ordinary selection expression. A simple factory, not Factory Method.
- A static method named
createis not sufficient evidence of the pattern.
Same Output, Different Mechanism
new PatternExamples.PlainJob().run("R1");
PatternExamples.simpleExporter(false).export("R1");- Both can produce
TEXT: R1. Identical output does not make their extension mechanisms identical. - One central selection function changes when an option is added; the other introduces a concrete creator subclass.
- Code structure and change requirements settle the classification.
Factory Method: Test, Trade-Off and Non-Use
- Tests exercise both concrete jobs and an instrumented creator whose hook returns a distinctive exporter: the inherited workflow uses the chosen product; null input is rejected before export.
- The hook-count assertion documents this tiny example. It is not advice to verify private call counts throughout the application.
- Factory Method gives an explicit extension point but creates inheritance coupling: readers follow the inherited workflow and its override.
When to Decline It
- A constructor accepting an
Exportercan be simpler when callers already know the product and need no creation hook. - So the running project keeps
ReservationApplication.create(sender)as a simple assembly function. - We do not convert it into a creator hierarchy solely to display the pattern.
- The separate example demonstrates the mechanism; the design decision explains when to decline it.
The Problem
- A receipt has two product roles: a heading renderer and a body renderer.
- Plain text needs plain versions of both; an HTML fragment needs HTML versions of both.
- Independent format flags can accidentally select an HTML heading and a plain body.
- A simple attempt is one renderer that switches format for every part: for two tiny operations that may remain the best implementation.
The Structure
ReceiptFamilyexposesheading()andbody().PlainFamilysupplies plain implementations of both roles;HtmlFamilysupplies HTML ones.- The
receiptfunction obtains both products from the supplied family and combines them. No format switch. - The essential distinction is the related set of product roles. Abstract Factory is not just a factory declared with Java’s
abstractkeyword.
Test the Relationship
- Render a complete receipt with each family:
LoanandR1as separate text lines in one; an HTML heading and paragraph in the other. - An angle-bracket input checks the HTML text-node escaping (
&,<,>). These are fragments, not a website or a general sanitiser. - A test that merely asserts both product references are non-null would miss a mixed-format family.
- The expected complete result observes the relationship that motivated the design.
Trade-Off: Which Axis Becomes Easy?
- Adding another family (say Markdown) can leave the client workflow unchanged: one new implementation supplying both roles.
- Adding a new product role (a footer) changes the factory contract: every family must supply it.
- One axis of extension becomes convenient by making another more coordinated.
Do Not Infer a Family From Proximity
- A clock and a borrower directory constructed near each other do not form a compatible product family.
- One sender with two implementations does not justify two product roles invented for the diagram.
- In the lending version, the small assembly function remains sufficient.
- If the real receipt stays this small, one formatter per output style is a reasonable conclusion.
The Problem: Configuration Hard to Read
- A report request: a required borrower identifier, an optional date interval, a returned-loan option, an ordered list of column labels.
- A positional constructor works, but calls with nulls, a boolean and a list become hard to review as options grow.
- A sequence of setters on the final request lets clients observe an incomplete request and change it after submission.
What Kind of Builder This Is
- A fluent builder with one product representation.
- The classical Builder discussion also separates construction steps from representations and, when useful, a director controlling the steps. This example does not claim that.
- Chaining methods alone is insufficient: what matters is staged construction followed by a validated, independent product.
Defaults and Invariants
- The required identifier must be nonblank.
- Omitting dates means no date restriction; supplying dates requires a positive half-open interval.
- Returned loans are excluded by default.
- Column labels form an ordered list, empty by default. The example stores labels but implements no report generation.
Validation Before Publication
build()checks the interval; the constructor copies the builder’s column list withList.copyOf.- Further builder changes cannot change a previously built request.
- The builder is mutable and used for one sequence at a time. An immutable product does not make its builder thread-safe.
- Settings remain until changed if the builder is reused.
The Constructor’s Checks
if ((builder.from == null) != (builder.toExclusive == null)) {
throw new IllegalArgumentException("Both dates or neither are required");
}
if (builder.from != null && !builder.from.isBefore(builder.toExclusive)) {
throw new IllegalArgumentException("Positive interval required");
}
columns = List.copyOf(builder.columns);- No partially valid request is ever returned.
Builder: Test, Trade-Off and Non-Use
- Tests check defaults, explicit options, invalid dates, and the independence of two products built before and after another column is added. Attempting to modify a product’s columns must fail.
- Checking only that chained calls compile would not detect an alias to mutable builder state.
- Costs: another type, more methods, a distinction between temporary state and a valid value.
- A small constructor with two meaningful required parameters is often clearer:
LoanPeriod(start, endExclusive)remains a constructor call.
The Problem
- The provider exposes
lookup(key)and returnsPerson(key, kind, status). - The application wants
find(borrowerId)returning a domainBorrowerwhen an eligible record exists. - The mismatch includes data meaning and failure behaviour, not only method names.
- Provider codes inside the coordinator would make reservation logic depend on their spelling; repeating translation in callers risks inconsistent defaults.
An Object Adapter
- It implements the application interface while holding the incompatible provider collaborator.
- The provider interface here is a local model of an external contract, not an HTTP client. A map-backed example makes every case reproducible.
- A real client later needs provider-specific integration tests.
Agree the Translation Contract First (1 of 2)
| Provider outcome | Application outcome |
|---|---|
Same identifier, kind S, status Y | Present student borrower |
Same identifier, kind T, status Y | Present staff borrower |
| Null row | Empty result: borrower absent |
Recognised kind, same identifier, status N | Empty result: borrower inactive |
Agree the Translation Contract First (2 of 2)
| Provider outcome | Application outcome |
|---|---|
| Unknown or null kind or status; mismatched identifier | DirectoryException: invalid provider data |
Provider’s declared Failure | DirectoryException retaining the cause |
| Unexpected programming exception | Propagates; never converted into absence |
- Codes are exact and case-sensitive.
Tmeans staff only because this teaching contract says so. No trimming, normalising or defaulting.
Error Precedence Is Part of the Contract
- The adapter validates the returned identifier and category before interpreting inactivity.
- So an inactive row with an unknown kind is still malformed:
Person("B1", "X", "N")is an error, not an empty result. - Covered by a test; another policy would need an explicit decision.
The Application-Owned Contract
BorrowerDirectorybelongs to the application’s vocabulary;LegacyDirectoryAdapterimplements it and delegates toLegacyDirectoryClient.Borroweris an immutable identifier/category value.Optional<Borrower>expresses a legitimate empty lookup; an exception expresses inability to obtain a trustworthy result.Optionalrepresents presence or absence. It does not define which failures should become absence.
The Adapter’s find (1 of 2)
LegacyDirectoryClient.Person row;
try {
row = client.lookup(borrowerId);
} catch (LegacyDirectoryClient.Failure failure) {
throw new DirectoryException("Borrower directory unavailable", failure);
}
if (row == null) return Optional.empty();
if (!borrowerId.equals(row.key())) throw new DirectoryException("Mismatched borrower id");The Adapter’s find (2 of 2)
BorrowerCategory category;
if ("S".equals(row.kind())) category = BorrowerCategory.STUDENT;
else if ("T".equals(row.kind())) category = BorrowerCategory.STAFF;
else throw new DirectoryException("Unsupported borrower category");
if ("N".equals(row.status())) return Optional.empty();
if (!"Y".equals(row.status())) throw new DirectoryException("Unsupported borrower status");
return Optional.of(new Borrower(row.key(), category));The New Entry Point
DirectoryReservationServicevalidates the local identifier, item and date, looks up the borrower, and calls the existing coordinator only after obtaining a valid domain borrower.- The caller supplies no category to this entry point.
- The coordinator still owns borrowing limits, interval conflicts, storage and notification.
The Service’s reserve
Objects.requireNonNull(start, "start");
var found = directory.find(borrowerId);
if (found.isEmpty()) return "Rejected: borrower not found or inactive.";
Borrower borrower = found.get();
if (!borrowerId.equals(borrower.id())) {
throw new DirectoryException("Directory returned a different borrower");
}
return coordinator.reserve(borrower.id(), itemId, start, days, borrower.category());Reading a Sequence Diagram
- A class diagram shows what exists; a sequence diagram shows what happens, in time order from top to bottom.
- Each box at the top is a participant; the vertical line below it is its lifeline.
- A solid arrow is a call; a dashed arrow is a return.
- A frame labelled
alt, split byelse, shows alternative paths: exactly one happens in a given run. - A note spans the participants it concerns.
What the Sequence Highlights
- The point before which reservation state must remain untouched.
- Malformed provider data follows the same no-mutation boundary as a provider failure, though its exception describes a different cause.
- Walk through
reserve("B2", "CAM1", 2026-10-20, 10):lookup("B2")returnsPerson("B2", "T", "Y");Tmaps toSTAFF; the unchanged coordinator applies the staff policy, stores R1, formats and makes its one notification attempt.
The Demonstration Output
Confirmed: R1 for B2 on CAM1 [2026-10-20, 2026-10-30).
Rejected: borrower limit or duration.
Rejected: borrower not found or inactive.
Reservations: 1
Notices: 1- Staff B2 is allowed ten days; student B1’s ten-day request is rejected; the missing identifier creates no record; only the accepted request creates a notice.
A Data-Backed Collaborator
- The directory is the book’s data-backed collaborator: the component that stands outside the application and answers from its own data.
- Chapter 1 promised it; Chapter 6 previewed its contract; Chapter 7 showed three doubles; this chapter builds the adapter; Chapter 10 places it in the application structure; Chapter 13 adds timeouts and an explicit fallback-or-failure policy.
- Here the provider is a synthetic in-process example: no transport, timeout or storage to fail.
The Same Questions for a Model
- Any component returning data or predictions is in the same position, including one that calls a trained model.
- What does an empty answer mean? What counts as a failure? Which outputs will you refuse to trust? How will a test substitute the component?
- For an ML model-serving domain: your adapter sits between your service and the model runtime. Map its label codes to your own types; treat a missing prediction and a timeout as different outcomes; reject values outside the documented range.
Rejection Is Not Failure
- A missing or inactive borrower produces the explicit rejection string.
- A directory outage throws
DirectoryException. - Returning “borrower not found” for both would make an operational failure look like a valid eligibility decision.
- Defaulting to staff would be worse: a longer loan without a trusted category.
An Entry Point, Not Authentication
- Knowing an identifier does not establish who the caller is or whether they may reserve for that person.
- The earlier coordinator APIs remain for trusted internal use, so callers can bypass the directory.
- A real exposed application needs a controlled authenticated entry path and reviewed authorisation rules.
- There is no caching, HTTP client, retry, persistence or cancellation in this increment.
Test the Boundary and Its Cost
- Synthetic rows: both category mappings, absence, inactivity, unknown codes, null fields, identity mismatch, expected failure translation.
- A service test: a staff record permits ten days; a student record rejects ten days using the same existing policies.
- Failure tests inspect state and notices, then perform a valid request to show the failure did not consume R1.
- Unexpected exceptions are not caught broadly.
The Cost of the Adapter
- A mapping layer and a contract that must evolve with provider changes.
- If two components already share the same interface and meaning, direct injection is sufficient.
- If they disagree on essential capabilities, an adapter cannot supply them.
- This implementation does not cache, retry, authenticate, persist or contact a real provider.
Acceptance Mapping (1 of 2)
| Promise | Executable evidence |
|---|---|
| Known active codes map correctly | mapsKnownActiveCategories (two rows) |
| Missing and inactive rows yield absence | absenceAndInactiveAreEmpty |
| Unknown or differently cased codes are not defaulted | unknownCodesAreErrors (four rows) |
| Null fields, mismatch, malformed inactive rows are errors | malformedRowsAreErrors |
Acceptance Mapping (2 of 2)
| Promise | Executable evidence |
|---|---|
| Invalid identifiers never reach the provider | invalidIdentifierDoesNotReachProvider (null, empty, blank) |
| Expected outage translated with its cause | translatesExpectedFailureAndKeepsCause |
| Unexpected failure remains visible | unexpectedProgrammingFailureIsNotDisguisedAsAbsence |
| Directory category selects the duration policy | categoryFromDirectoryControlsReservationPolicy |
| Absence and outage change nothing and keep R1 | absenceAndOutageDoNotStoreNotifyOrConsumeId |
The Problem
- An isolated notification exercise needs optional audit observations and a single retry after a declared delivery failure.
- Subclasses for every combination multiply quickly. Flags on one large sender combine responsibilities and hide order.
- A Decorator implements the same interface as its wrapped delegate and adds behaviour around delegation.
- Java’s
FilterInputStreamillustrates the wrapping mechanism: an input stream containing another input stream.
The Two Decorators
// AuditedSender.send
events.add("attempt");
try { delegate.send(message); }
catch (DeliveryFailure failure) { events.add("failed"); throw failure; }
events.add("sent");
// RetryOnceSender.send
try { delegate.send(message); }
catch (DeliveryFailure failure) { delegate.send(message); }What the Stand-In Does Not Promise
- The audit list is an in-memory observation example, not durable audit storage.
- If a real audit sink can fail, decide whether to propagate that failure, preserve the original delivery failure, or report it separately.
- A real sender may deliver and then lose the acknowledgement: a retry can duplicate the effect. This toy has no idempotency key.
- “Sent” means only that the delegate returned normally.
- A same-signature wrapper is not automatically behaviourally substitutable under every error policy.
Decorator: Test, Trade-Off and Non-Use
- Tests assert both event sequences, the two-attempt bound, and propagation of expected and unexpected failures.
- The decorators stay isolated. Chapter 6’s notification contract says one attempt, no automatic retry: installing this retry there would change that contract. A pattern name does not authorise it.
- Use one straightforward sender when optional combinations are unnecessary. Decorators add another path to trace and can obscure which layer handles an error.
Compare by the Decision Isolated (1 of 2)
| Observed pressure | Candidate | Simpler alternative |
|---|---|---|
| Subclasses must create a workflow’s product | Factory Method | Inject a ready product into the workflow |
| Related product roles must vary together | Abstract Factory | One formatter or one assembly function |
| Many named options configure an immutable value | Builder | A clear constructor or named creation method |
Compare by the Decision Isolated (2 of 2)
| Observed pressure | Candidate | Simpler alternative |
|---|---|---|
| Provider interface and meanings differ | Adapter | Direct injection if the contracts already match |
| Optional responsibilities compose around one interface | Decorator | One explicit implementation with fixed behaviour |
- Explain who owns the variation, how failures travel, and how the structure changes future edits.
What the Running Project Gained
- Chapter 8 adds the directory-backed reservation service, domain borrower and provider adapter.
- The Chapter 7 coordinator, policies, availability query, loan service and notification implementations remain unchanged. The original test suite stays in the build.
- New claims: a staff example can request ten days through a category-free entry point; a student cannot; missing or inactive borrowers are rejected before storage; provider failures are identifiable and leave state untouched.
The Inherited Limits Remain
- Returned loans do not remove reservations, free their intervals or restore counted allowances.
- The project still runs in memory within one process.
- Pattern work does not resolve these separate requirements.
- The build executes 63 JUnit invocations: 35 inherited, 17 directory-boundary cases and 11 pattern cases.
Record a Decision Another Developer Can Challenge
Provider codes and null/error conventions differ from the application. An object adapter translates them behind
BorrowerDirectory. This keeps provider details out of reservation policy, at the cost of mapping tests and explicit provider-contract maintenance.
- Then name what was declined.
What Was Declined
- The assembly function needs no creator hierarchy.
- No related provider product family is required.
- The core value objects do not need builders.
- Retry would change the notification promise.
- Declining those structures is part of the design outcome.
- If the only explanation is “this chapter teaches the pattern”, keep it in the teaching examples, not the running system.
Ground Rules for the Activity (1 of 2)
| For this activity | |
|---|---|
| Your goal | Classify each suggested structure by what it actually does, and decide which are justified |
| You start with | The isolated pattern examples, the directory contract in 8.5 and the Chapter 6 notification contract |
| What is in bounds | Classify, correct and decline; wire no pattern into the application without a requirement |
Ground Rules for the Activity (2 of 2)
| For this activity | |
|---|---|
| How to check | The source (is there a subclass hook?), the mapping table, tests for unknown codes, outage, immutability and decorator order |
| How to decide | A verdict with a structural or behavioural reason for each claim |
| No AI account? | The ready-made example is enough |
The Ready-Made Example (1 of 2)
Author-written fictional AI output.
Rename the static
createfunction toFactoryMethodand it becomes Factory Method. Add an Abstract Factory even though there is only one product role. The adapter should catch every exception and return a staff borrower to keep the system available.
The Ready-Made Example (2 of 2)
Wrap all senders in retry decorators; order does not matter. A builder means the finished object is immutable even if it retains the builder’s mutable list.
- Identify the unsupported claims. For each, name the missing requirement or structural evidence and a concrete test.
Solution (1 of 2)
| Fictional claim | Diagnosis and correction |
|---|---|
Renaming static create makes Factory Method | Names do not create a subclass hook: compare simpleExporter with ExportJob.createExporter |
| One product role requires Abstract Factory | No related set has been established: keep simple assembly |
| Catch every exception and return staff | Hides failure and grants an unsupported category |
Solution (2 of 2)
| Fictional claim | Diagnosis and correction |
|---|---|
| Wrap all senders with retry | Changes the one-attempt contract: keep retry isolated |
| Decorator order does not matter | Changes which layer observes each attempt: the two exact audit-history tests |
| A builder guarantees immutable products | Mutable aliases can remain after build: snapshot and unmodifiable-list tests |
- Pattern classification needs source inspection as well as output tests.
Three Wrong Candidates to Diagnose
// 1. an unfamiliar code silently acquires staff privileges
category = "S".equals(row.kind()) ? STUDENT : STAFF;
// 2. turns provider or programming failures into absence
catch (RuntimeException failure) { return Optional.empty(); }
// 3. the product shares the builder's mutable list
columns = builder.columns;- Each was applied to a copy and rejected by named tests:
unknownCodesAreErrors,translatesExpectedFailureAndKeepsCause,builtValueIsIndependentOfReusableBuilder.
Mistakes 1 to 3
| Mistake | Consequence | Better question |
|---|---|---|
Classify by names such as create or Factory | Misidentifies the extension mechanism | Which operation varies, and how? |
| Put provider codes inside borrowing policies | Couples business rules to provider representation | Where should translation live? |
| Treat absence and outage identically | Hides failure as an eligibility decision | Was a valid lookup completed? |
Mistakes 4 to 6
| Mistake | Consequence | Better question |
|---|---|---|
| Assume a family interface prevents all mixing | Overstates guarantees | Do concrete factories return compatible products? |
| Keep the builder’s mutable list | Previously built values can change | Does construction copy mutable state? |
| Assume decorator order is irrelevant | Changes audit meaning and failure visibility | Which calls pass through each layer? |
Input and Demonstrations
- You start with: the Chapter 7 version, the synthetic provider contract and mapping table, and the Chapter 8 code file. Steps 1 to 8: the book’s project; **step 9: your team’s project.
- Demonstrations: first compare the simple factory and the overridden creation hook; then trace an active borrower and a failed lookup through the adapter boundary.
- Guided practice: add an independently chosen malformed provider case and explain why it produces failure, not absence. Run the decorator tests and predict both audit histories first.
Independent Work: Steps 1 to 3
- Integrate the borrower-directory boundary using the selected mapping contract.
- Show staff and student duration outcomes without accepting a caller-supplied category at the new entry point.
- Prove that missing, malformed and failed lookups create no reservations or notices.
Independent Work: Steps 4 to 6 (Optional)
- Compare two designs for a second scenario: injected exporter versus Factory Method, or constructor versus Builder.
- Write a problem-solution-consequence decision, including a justified non-use case.
- Inspect the fictional AI answer and retain independent verification of your corrections.
Independent Work: Steps 7 to 9
- Builder exercise: in the isolated
patternspackage, add an optional maximum-row setting. Absent by default; rejects zero and negative; does not change an already built request. Write the tests first. - Decorator exercise: add a third decorator that counts attempts. Predict the count for one failure then success when it sits outside the retry and inside it; check with tests.
- On your team’s project, find a place where your code uses an external or mismatched interface directly. Decide whether an adapter is justified using problem-solution-consequence, and say what you would decline to build.
Builder Exercise: A Solution
public Builder maximumRows(int value) {
if (value <= 0) throw new IllegalArgumentException("Positive row limit required");
maximumRows = OptionalInt.of(value);
return this;
}OptionalInt maximumRows = OptionalInt.empty();in the builder; copied at construction.OptionalIntis immutable, so copying the reference keeps a built request independent.- Absence is explicit. A sentinel such as
0for “no limit” is weaker: it makes “zero rows” unrepresentable.
Decorator Exercise: The Prediction
public static final class CountingSender implements Sender {
public void send(String message) {
attempts.incrementAndGet();
delegate.send(message);
}
}- One failure then success. A counter outside the retry sees one logical call: 1. A counter inside sees both delegate attempts: 2.
- The same order effect as the audit histories, measured by a number. With these three tests,
PatternExamplesTestruns 14 tests.
What You Should End Up With, and When You Are Done
- What you should end up with: runnable source and tests; a mapping table; a reproducible demonstration; a short design decision; the AI claims record; the two exercise solutions with tests; the step 9 note.
- Accept when: known codes map correctly; unknown codes never grant a category; provider outage is distinct from absence; legacy behaviour remains tested; the second comparison identifies costs, not counts of classes.
- If you want more: a changed provider code for staff, or a second provider. Do not add automatic fallback between providers without defining conflict, outage and identity semantics.
An Example Decision Note
- Problem: the directory’s
lookup, raw codes, null absence and provider exception differ from the application’s needs. - Solution: an application-owned
BorrowerDirectoryand an object adapter; a service looks up a borrower before invoking the coordinator with the mapped category. - Consequences: translation in one tested boundary; several small types and mapping tests; local examples cannot establish real-provider compatibility.
- Revisit when: a real SDK contract exists, an authenticated entry point is designed, or a requirement adds a related family of integration products.
Concept Questions
- What distinguishes the worked Factory Method from
simpleExporter? - Why are two unrelated construction methods insufficient justification for Abstract Factory?
- Which property makes the built report request independent of subsequent builder changes?
- How do Adapter and Decorator differ even though both may contain another object?
Diagnosis Questions
- A directory adapter catches
RuntimeExceptionand returns an empty result. Describe at least two different failures this hides and the resulting problem for the application. - A team changes
Audit(Retry(sender))toRetry(Audit(sender))because both satisfySender. Predict the audit histories for one failure followed by success and explain the requirement that selects between them.
Guidance: Questions 1 and 2
ExportJobowns an inherited workflow and subclasses override a creation hook.simpleExporterselects and returns a product in an ordinary function. A method namedcreatedoes not alone establish the former.- A genuine family needs several product roles that must vary together for a compatibility reason (heading and body share an output format). Constructing an unrelated clock and directory together does not.
Guidance: Questions 3 and 4
- The request copies mutable builder state, validates before publication and exposes no mutation operation. Returning a final reference to the builder’s mutable list would not suffice.
- Adapter translates one contract into the contract its client expects. Decorator keeps the component interface and adds optional responsibilities around delegation. Holding another object is a mechanism, not a classification.
Guidance: Questions 5 and 6
- It conceals a legitimate provider outage and a programming defect (say an unexpected illegal state). Both look like a completed lookup that found no borrower: a misleading rejection, and operators lose the cause.
- Audit outside retry:
attempt, sent. Retry outside audit:attempt, failed, attempt, sent. Choose the former when the record concerns logical operations, the latter when every delivery attempt must be observed.
Transfer Challenge: A Library Application
A library application receives patron records from an external system using numeric category codes and a separate inactive flag.
- Design the application-owned lookup contract, an adapter mapping and three failure cases.
- Decide whether constructing its receipt output needs a simple factory or a family factory.
- Explain what additional requirement would change your answer.
Transfer Guidance
- Obtain an explicit provider mapping first. Hypothetically: category 10 student, 20 staff, an inactive flag excludes an otherwise valid patron. Do not transplant the equipment example’s string codes.
- An application-owned
PatronDirectory.find(id); immutable patron values; eligibility policy stays outside the translation layer. - Failures: unknown category, returned identifier differs, declared outage: all prevent the borrowing operation before state changes; the outage keeps its cause.
- One renderer: direct injection or simple factory. Several matching plain/HTML components: Abstract Factory becomes a candidate.
Summary
- Factory Method varies product creation through a subclass hook.
- Abstract Factory groups related product roles.
- Builder stages construction.
- Adapter translates incompatible contracts.
- Decorator composes optional responsibilities whose order still matters.
- Creation and connection patterns are useful when they isolate a specific decision.
Sources and Further Reading
- Gamma, Helm, Johnson, Vlissides: Design Patterns: Elements of Reusable Object-Oriented Software.
- Gamma et al.: the Abstract Factory and Adapter publisher excerpts.
- Oracle: Java SE 17
OptionalandFilterInputStream. - JUnit Team: JUnit 5.13.4 User Guide.
- Martin: Agile Software Development (2002).
- Full references and links are in the chapter text.