Chapter Roadmap (1 of 3)
- 6.1 Why dependencies make change hard
- A real boundary: notification output, first built the coupled way.
- 6.2 Designing an interface around what the client needs
- Design the interface around what a particular client needs.
- 6.3 Who decides what, and what happens when notification fails
- Three decisions, and the partial-success case made explicit.
- 6.4 Who owns the abstraction? Dependency inversion
- Source dependency direction; DI can exist without DIP.
Chapter Roadmap (2 of 3)
- 6.5 Handing collaborators in: injection and the composition root
- Two views of one design: classes and objects.
- 6.6 Inversion of control and containers: what do they add?
- An educational container makes lifetime visible.
- 6.7 What abstraction costs
- 6.8 Reviewing an interface that hides very little, with AI
Chapter Roadmap (3 of 3)
- 6.9 Dependency habits that mislead
- 6.10 Try it yourself: assemble a replaceable output
- Nine steps; step 9 is on your team’s project.
- 6.11 Questions to think with, 6.12 A different setting: the transfer challenge
- 6.13 Revising for Quiz 1: where to look
- What to take away, and what comes next
- Chapter 7: test design, controlled substitutes, red-green-refactor.
Where This Chapter Fits and Where You Start
- Supports LO1 and LO4. Quiz 1 revision points appear later and refer to Chapters 2 to 5, matching its Weeks 1 to 4 coverage. This chapter’s material is not silently added to that coverage.
- You start with: Chapter 5’s Java 17 code: student and illustrative staff policies, extracted formatter, the original 34 checks, the five acceptance groups and the 44 Chapter 5 checks.
- Formatting a string has never meant sending email. Notification output is a new, explicitly bounded teaching feature.
How to Use This Chapter
- Core reading: Sections 6.1 to 6.6. Section 6.6 compares manual assembly with a small educational container.
- What you must do: steps 1 to 9 of Section 6.10, including step 7 (the container walkthrough). Steps 1 to 8: the book’s project. Step 9: your team’s project.
- If you want more: the AI activity, the extension in 6.10, the questions and the transfer challenge.
- Readings: the DIP and ISP chapters of Martin, and Fowler’s article on dependency injection.
Opening Scenario: A Console Sender Inside the Coordinator
- The coordinator now uses separate borrowing policies and a confirmation formatter.
- A new requirement: pass each accepted reservation to a notification output.
- One member creates a console sender inside the coordinator. It works.
- But another member cannot inspect notifications without watching console output.
- Replacing the output requires editing the reservation workflow.
An AI Proposes an Interface
- The coordinator’s field becomes
NotificationSender. - But its constructor still creates a particular sender and reads provider settings.
- The diagram looks different. The code still chooses its own environment.
- Naming an abstraction is not the same as depending on a useful contract or being supplied a collaborator from outside.
What a Dependency Is
- A dependency exists when one part of the program needs knowledge or behaviour from another.
- Dependencies are necessary: the coordinator needs dates, reservations and borrowing decisions.
- The design problem: which knowledge should remain stable, and where should changing details live?
What Chapter 5 Left Behind
- Two visible decisions inside the coordinator: construction of concrete policies and selection of a policy by category.
- The formatter was also constructed there.
- A deliberate intermediate version.
- Now: move assembly out of the request workflow while introducing a notification boundary.
The New Requirement
After storing an accepted reservation, attempt one notification through the configured output. Rejected or invalid requests produce no notification. The configured output may record the accepted reservation in memory or print a notice to a supplied stream.
- Local teaching outputs: not an email service, durable queue or proof of receipt.
- A normal return means the output completed its own local operation, not remote delivery.
The Deliberately Coupled Version
private final NotificationSender notifications =
new ConsoleNotificationSender(System.out, new ConfirmationFormatter());- The field has an interface type, but the coordinator still:
- names the console implementation,
- chooses the global output stream,
- constructs its formatting dependency.
- The interface alone has not separated configuration from use.
Feature Change, Then Structural Change
- This intermediate version is newly introduced teaching code, not a description of Chapter 5.
- Adding notification is a feature change.
- Replacing direct construction with an injected collaborator is a subsequent structural change, with an explicit constructor migration.
- The concrete pressure: one caller wants console output; another wants to inspect accepted records in memory. The same algorithm should serve both.
Decisions That Should Remain in the Coordinator
- The coordinator still decides when a reservation has been accepted, and so when notification is eligible.
- Moving that decision into the sender would duplicate policy and conflict handling.
- The sender decides how its local output handles the accepted record, without granting or rejecting anything.
A Reviewable Dependency Diagnosis
- “Remove all dependencies” is not a useful goal.
- “Remove the coordinator’s knowledge of which notification provider to construct, while keeping its acceptance sequencing” is reviewable.
- A good diagnosis lists both the unwanted choice and the decision that belongs in the client.
- Output failure needs an application-level answer too: injection does not settle it.
A Broad Interface to Avoid
- An all-purpose
EquipmentPlatforminterface with:send,registerBorrower,exportInventory,deleteAccount,configureMailServer,rotateCredentials.
- The coordinator uses only the first. The rest expose unrelated responsibilities and encourage implementations that supply meaningless methods or throw unsupported-operation exceptions.
What a Narrow Role Gives
- A console output and an in-memory recorder implement what they genuinely support.
- The role does not need every capability of a future provider.
- An administration tool may need a different contract, if that requirement actually exists.
- Do not create an administration interface until an administration client exists.
Small Is Not the Same as One Method
- The notification contract happens to have one method. ISP does not prescribe one-method interfaces.
- A client that genuinely needs several cohesive operations may benefit from a role containing all of them.
- Example: a document-writing client needing both writing and closing a session. Splitting them only to cut method count could allow a write without the required lifecycle management.
Two Views of One Object
received()is an inspection operation on the concrete recorder.- The coordinator does not need it, so it is not on
NotificationSender. - A check can keep a reference to the recorder while supplying the same object through the narrow sender role.
- A deliberate use of two views of one object.
Specify Outcomes and State Effects
- The sender contract: a non-null accepted reservation; it must not modify that record or the coordinator; expected output failures use
NotificationException. - The recorder appends the immutable record to its own list. The console prints a notice to its supplied stream.
- Both have output effects, so this is not a pure policy operation. Chapter 5’s
BorrowingPolicyremains side-effect-free under its contract. - Do not apply one collaborator’s restrictions blindly to another.
Why the Sender Receives a Reservation
- This keeps the notification tied to the accepted record, not raw identifiers and dates assembled afresh.
- It means the boundary deliberately knows that small domain type.
- If an external transport later needs a different representation, an adapter can translate it. That need does not justify a transport-specific payload now.
Do Not Mirror a Provider SDK
- A provider SDK might expose account management, diagnostics and bulk operations.
- A client-specific interface should express the application’s needs, not the SDK.
- Review question: can the contract be explained without referring to a vendor or transport setting?
- “Send this accepted reservation to the configured output”: yes.
- “Send this vendor request with the provider’s retry token”: no.
The Recorder
public final class RecordingNotificationSender implements NotificationSender {
private final List<Reservation> received = new ArrayList<>();
@Override
public void send(Reservation reservation) {
received.add(Objects.requireNonNull(reservation, "reservation"));
}
public List<Reservation> received() { return List.copyOf(received); }
}The Console Sender
public void send(Reservation reservation) {
output.println("Notice: " + formatter.format(reservation));
if (output.checkError()) {
throw new NotificationException("Console output failed.");
}
}PrintStreamnormally records I/O errors rather than throwing, so the sender checkscheckError()and translates the expected local error.- The error state is sticky: once reported, later notices through the same sender are reported as failed too.
Separate Three Decisions
| Decision | Owner in this version | Reason |
|---|---|---|
| Whether the request is acceptable | Coordinator and selected borrowing policy | Depends on reservation rules and current records |
| How notification is represented locally | Sender implementation and formatter | Depends on the chosen output |
| Which concrete objects are used | Application composition root | Depends on the demonstration environment |
Moving Assembly Does Not Remove It
- The first decision should not depend on console configuration.
- The last decision must exist somewhere: a running program needs concrete objects.
- Moving assembly to the boundary makes it explicit rather than pretending it disappears.
- “Policy” has levels: a numeric borrowing policy decides limits; the coordinator implements the broader application policy (store before notifying; handle expected failure).
The Partial-Success Case
- The completed operation stores the reservation, formats the confirmation and calls the sender.
- On an expected
NotificationExceptionit returns the confirmation followed by:
Notification failed; reservation retained.- The leading space joins the suffix to the confirmation.
- The record remains stored and counts as active. It is not deleted, not reported as rejected, and not retried.
Why the Rule Matters
- An author-chosen rule invented for this book. Another application might choose differently, but must make storage and communication consequences explicit.
- A result that just says “failed” can encourage callers to repeat a reservation request that already succeeded.
- Repeating it here normally gives an overlap rejection. It is not a notification retry.
- There is no queue or worker: do not call a failed notification “pending”.
Invocation Is Not Delivery
- The coordinator attempts the sender once per successful reservation call: a local control-flow fact, not an exactly-once delivery guarantee.
- A future remote sender might deliver and then fail before confirming. Retrying could duplicate the delivery.
- Even the console may partially write before reporting an error.
What the Coordinator Catches
- It catches that specific exception.
- Unexpected programming failures are not converted into apparent success.
- Such failures may still occur after storage. The example does not claim transactional atomicity across formatting, storage and output.
- A richer result model and reliable delivery design belong to later integration work.
Acceptance Examples Before Refactoring (1 of 2)
| Example | Reservation result | Notification |
|---|---|---|
| Eligible available request | Exact confirmation; one record stored | One accepted record supplied to the sender |
| Same-item overlap | Existing rejection; records unchanged | No call |
| Invalid duration | Existing rejection; records unchanged | No call |
Acceptance Examples Before Refactoring (2 of 2)
| Example | Reservation result | Notification |
|---|---|---|
| Invalid identifier | Existing exception; records unchanged | No call |
| Expected output failure after acceptance | Confirmation plus failure suffix; record retained | One attempt; no automatic retry |
| Repeat of the accepted request | Overlap rejection | No second attempt |
- These constrain the feature whether assembly is manual or container-based.
DIP: Source Dependency Direction
- DIP separates higher-level policy from changing implementation details through abstractions.
- The coordinator names
NotificationSender, an application-owned role. Concrete outputs implement it. - The contract does not import a provider’s SDK type or inherit its configuration vocabulary.
- At runtime the coordinator still calls a concrete object. DIP does not reverse that call: it changes what the source needs to know and which side defines the abstraction.
Reading Figure 6.1
- Solid arrows are associations: the coordinator holds a
NotificationSender, aConfirmationFormatterand aMap<BorrowerCategory, BorrowingPolicy>. - Dashed lines with a hollow triangle are realisations.
- Dashed arrows labelled “creates” show which class’s source names which concrete class.
- The recorder is an alternative implementation, not an additional mandatory destination: the application creates only the console sender.
Compare With Figure 5.1
- There, two “creates” arrows ran from the coordinator to the concrete policy classes.
- Here, every “creates” arrow starts at
ReservationApplication. - The coordinator’s outgoing arrows end at roles, apart from the concrete
ConfirmationFormatter, which stays concrete on purpose. - The change in arrow direction is visible evidence that its source no longer chooses its environment.
Source Knowledge, Not Time Order
- The arrows show source-level knowledge, not time order.
- The composition root knows the concrete console implementation because choosing it is the root’s job.
- The files still use the unnamed Java package. Contract ownership is a design decision, not a build-enforced boundary. Chapter 10 organises larger application boundaries.
DI Can Exist Without DIP
- The coordinator receives
ConsoleNotificationSenderthrough its constructor: that is dependency injection. Someone else supplies the collaborator. - It still depends on the concrete provider type: the boundary is not yet inverted.
- Conversely, a field typed
NotificationSenderassignednew ConsoleNotificationSender(...)inside the coordinator uses an abstraction while keeping concrete construction knowledge. This is the intermediate defect.
A Preview: An Outside Data Source Behind a Contract
/** Empty means absent or inactive, never a provider outage. */
@FunctionalInterface
public interface BorrowerDirectory {
Optional<Borrower> find(String borrowerId);
}- Chapter 8 builds this equipment-directory contract. Not part of this version.
- Empty means “no such active borrower”; an outage is a different outcome (an exception); the contract uses the application’s own types.
Not Every Concrete Dependency Is Harmful
- The coordinator still constructs accepted
ReservationandLoanPeriodvalues and uses an in-memory list. Creating a small domain value differs from selecting an environmental output provider. - The concrete
ConfirmationFormatteris supplied in the constructor, but has no interface: a substitutable formatting role has not been required. - Using Java’s
Mapinterface does not itself demonstrate DIP. The decision is that policy mappings are assembled outside the coordinator.
The Completed Constructor
public ReservationCoordinator(
Map<BorrowerCategory, BorrowingPolicy> policies,
ConfirmationFormatter formatter,
NotificationSender notifications) {
this.policies = Map.copyOf(Objects.requireNonNull(policies, "policies"));
for (BorrowerCategory category : BorrowerCategory.values()) {
if (!this.policies.containsKey(category)) {
throw new IllegalArgumentException("Missing policy: " + category);
}
}
this.formatter = Objects.requireNonNull(formatter, "formatter");
this.notifications = Objects.requireNonNull(notifications, "notifications");
}What the Constructor Checks
- Null values, and an entry for every current borrower category.
Map.copyOfproduces an unmodifiable map and rejects null keys or values. A caller cannot change the coordinator’s mapping by editing the original map later.- The copy is shallow: it does not clone the policy objects. The supplied policies are stateless; a mutable or dishonest policy would not be made safe.
- Missing configuration fails at construction, not halfway through a request.
The Request, Now With Notification
reservations.add(reservation);
String confirmation = formatter.format(reservation);
try {
notifications.send(reservation);
} catch (NotificationException failure) {
return confirmation + " Notification failed; reservation retained.";
}
return confirmation;policies.get(category)replaces the former categoryswitch.
Assemble Once at the Boundary
public static ReservationCoordinator create(NotificationSender sender) {
return new ReservationCoordinator(
Map.of(BorrowerCategory.STUDENT, new ReservationRules(),
BorrowerCategory.STAFF, new StaffReservationRules()),
new ConfirmationFormatter(), sender);
}mainchooses a console sender with a supplied stream and starts the demonstration.- A check chooses a recorder instead.
A Check Chooses a Recorder
RecordingNotificationSender inbox = new RecordingNotificationSender();
ReservationCoordinator coordinator = ReservationApplication.create(inbox);- The check can later inspect
inbox.received()because it retained the concrete reference. - The coordinator only sees the sender role.
- Replaceability without a mocking library. Chapter 7 develops the vocabulary of test doubles.
Two Views, Not One
- Figure 6.1 says which classes know about which. Figure 6.2 says which objects were wired to which.
- The sender also receives its own formatter and output stream, left out for readability.
- Neither shows the order of calls: the coordinator checks eligibility and conflicts, stores, formats, then calls the sender already supplied.
- Do not read event order from either diagram.
The Composition Root
- A composition root is the small application-entry area where concrete collaborators are assembled.
- It may call helper factory methods. They do not become a service locator merely because they are static.
- What matters: the coordinator is passed its dependencies and does not call back into a global registry to find them.
The Constructor Migration, Honestly Stated
- The no-argument coordinator constructor is removed. Callers must supply dependencies, normally through
ReservationApplication.create. - The completed source is not a drop-in constructor-compatible replacement for Chapter 5.
- Existing checks and demos are migrated only at their construction expressions; their reservation assertions stay the same.
- Claiming every old source line is unchanged would be inaccurate.
What Stays the Same
- The four-argument
reservestill means student borrowing; the explicit category overload still uses the trusted category supplied. - Student and staff limits, date boundaries, count behaviour and rejection messages remain as before on ordinary paths.
- The new notification side effect and failure suffix are separate feature behaviour.
Constructor Injection vs. Later Setters
- Required dependencies fit constructor injection: an object cannot be used before they are supplied.
- With later setters, the program may contain an unusable coordinator, and every operation may need to ask whether configuration has happened.
- Setters are not universally wrong: optional or intentionally replaceable runtime collaborators have a different lifecycle. This coordinator has no requirement to swap its sender while processing.
Distinguish the Terms
- Dependency injection: supplying collaborators from outside an object.
- Inversion of control: another component controls some aspect of execution or assembly the object would otherwise control.
- DI container: a tool that uses registration or configuration to assemble and often manage objects.
- The manual coordinator already uses DI. A container automates repeated assembly; it cannot decide whether the contract or failure policy is right.
Terms and the Question Each Answers (1 of 2)
| Term | Question answered in this example |
|---|---|
| DIP | Does the coordinator depend on the application’s notification role rather than a provider detail? |
| DI | Who supplies the sender, formatter and policy mapping? |
| IoC | Which component controls assembly or later calls into application code? |
The Educational Container
DemoContainer, inContainerWalkthrough.java: isolated from the running application, single-threaded.- It registers a provider function for a type and lazily creates one instance of each registered type.
- It caches those instances, reports a missing binding and detects a construction cycle.
ReservationCoordinatorhas no container import, field or lookup call.
The Container
static final class DemoContainer {
private final Map<Class<?>, Supplier<?>> providers = new HashMap<>();
private final Map<Class<?>, Object> instances = new HashMap<>();
private final Set<Class<?>> constructing = new HashSet<>();
public <T> void bind(Class<T> type, Supplier<? extends T> provider) { /* ... */ }
public <T> T get(Class<T> type) {
if (instances.containsKey(type)) return type.cast(instances.get(type));
Supplier<?> provider = providers.get(type);
if (provider == null)
throw new IllegalArgumentException("Missing binding: " + type.getSimpleName());
if (!constructing.add(type))
throw new IllegalStateException("Dependency cycle: " + type.getSimpleName());
/* run the provider, cache, return */
}
}The Registrations
container.bind(ConfirmationFormatter.class, ConfirmationFormatter::new);
container.bind(NotificationSender.class, RecordingNotificationSender::new);
container.bind(ReservationCoordinator.class, () -> new ReservationCoordinator(
Map.of(BorrowerCategory.STUDENT, new ReservationRules(),
BorrowerCategory.STAFF, new StaffReservationRules()),
container.get(ConfirmationFormatter.class),
container.get(NotificationSender.class)));- The provider calls the same ordinary constructor. Only configuration code uses the container.
Trace the First Resolution
- Registration stores a provider without running it.
- The first coordinator resolution runs its provider, which requests the formatter and sender.
- Those providers run; their instances are passed to the constructor.
- The coordinator is cached.
- A second resolution returns the same coordinator: the reservation stored through the first reference is visible through the second.
Lifetime Is a Design Decision
- Sharing the coordinator means sharing its in-memory reservation list.
- A container does not make that list thread-safe.
- A fresh coordinator per request would discard earlier reservations unless storage were shared elsewhere.
- Neither lifetime is automatically correct for a future server application.
Configuration Failures
Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22).
Repeated resolution shares the same coordinator and one reservation.
Missing binding: NotificationSender
Dependency cycle: NotificationSender- A missing registration and a cycle fail with identifiable configuration errors, not business-rule rejections.
- Zero exit status means the walkthrough observed the intended cases.
The Container’s Limits Are Deliberate
- No reflection, annotation scanning, concurrent resolution, scopes beyond one cached instance per type, or resource cleanup.
- Do not extend it into a home-grown production framework.
- Its purpose: expose registration, resolution and lifetime in ordinary Java.
- Real frameworks (such as Spring’s IoC container) supply much more machinery. The manual graph gives you a basis for reading such configuration.
Whichever Container You Use
- Identify its registrations, supplied constructor arguments and instance lifetimes before adding annotations.
- The library or framework name should not replace an explanation of the object graph.
- A library container arrives with libraries of its own, which the plain
javacgate does not manage. The lab uses the educational container, so everything needed is in the chapter and companion.
Each Boundary Earns Its Place
- The notification interface earns its place: the same client needs two outputs and a predictable failure boundary.
- The borrowing-policy interface already expresses a genuine variation.
- The formatter remains concrete. No interface on immutable
ReservationorLoanPeriod. - The root may know concrete classes. Hiding every choice behind another factory makes the program hard to trace without protecting any additional change.
The Policy Map Has a Cost Too
- It removes a category switch from the coordinator but adds a configuration invariant: every category must have a non-null policy.
- Constructor validation makes that cost visible.
- A design explanation should include it, rather than presenting fewer conditionals as an unconditional improvement.
Recognise a Disguised Dependency
- An injected
ApplicationContextwith arbitrary lookups hides all the coordinator’s real needs. A method callingcontext.get("mail")is still locating its own collaborator. - An interface whose
sendneeds server addresses, passwords and transport-specific objects moves provider setup into every caller. - The interface exists syntactically; the application is still coupled to implementation details.
What the Chapter Has Not Solved
- An in-memory, sequential teaching application.
- No authoritative borrower categories, persistent notification history, retry queue, remote delivery, cancellation or availability enquiry.
- It does not implement the whole MVP reservation-and-loan flow.
- The new structure makes selected changes and tests easier. It does not manufacture those features.
Ground Rules for the Activity (1 of 2)
| For this activity | |
|---|---|
| Your goal | Judge whether a generated refactoring really separates configuration and policy from the reservation workflow |
| You start with | The coupled coordinator from 6.1, the notification requirement and Figure 6.1 |
| What is in bounds | Evaluate the proposal and draw a corrected object graph; do not apply it wholesale |
Ground Rules for the Activity (2 of 2)
| For this activity | |
|---|---|
| How to check | The coordinator’s source (which concrete classes does it name?), the recorder-based checks, the failure case |
| How to decide | A verdict with a concrete reason for each proposal, then a corrected constructor |
| No AI account? | The ready-made example is enough |
The Ready-Made Example (1 of 2)
Author-written fictional AI output.
Add
NotificationSenderand type the coordinator field with it. Inside the coordinator constructor, read the provider settings and createConsoleNotificationSender. Addconfigure,exportHistoryanddeleteBorrowerto the same interface so it can support future features.
The Ready-Made Example (2 of 2)
Catch every runtime exception and still return the ordinary confirmation. Pass a global container into the coordinator so future dependencies can be found when needed. The new interface means DIP and DI are both complete.
- Find where configuration still occurs, what the client actually needs, and what a caller would learn about failure.
- Explain receiving a sender versus receiving a registry.
Model Critique (1 of 3)
| Claim or change | Verdict | Reason |
|---|---|---|
Type the field as NotificationSender | Useful but insufficient alone | It expresses a role, but concrete construction can remain |
| Read provider settings inside the coordinator | Reject | Environmental configuration still changes the workflow’s source |
| Construct the console sender inside the coordinator | Reject for the final design | The caller cannot select a recorder without editing the client |
Model Critique (2 of 3)
| Claim or change | Verdict | Reason |
|---|---|---|
| Add configure/export/delete to the sender role | Reject without a client need | They couple this client to unrelated capabilities |
| Catch every runtime exception, return ordinary confirmation | Reject | It hides programming defects; expected failure becomes indistinguishable from success |
Model Critique (3 of 3)
| Claim or change | Verdict | Reason |
|---|---|---|
| Inject a global container for future lookups | Reject | Real required collaborators become hidden behind a registry |
| Declare DIP and DI complete because an interface exists | Reject | Contract ownership, provider knowledge and actual assembly must be inspected |
- Corrected answer: three explicit constructor parameters, the application-owned sender contract, two implementations, external assembly; the stated notification timing and failure outcome; the constructor migration named; the still-limited in-memory model.
Mistakes 1 to 3
| Mistake | Why it is misleading | Better action |
|---|---|---|
| An interface-typed field proves DIP | Concrete construction or provider-specific types may remain | Inspect the actual source dependencies |
| DI means a framework | Ordinary constructors can receive dependencies | Explain assembly before adding tooling |
| Every class needs an interface | Many stable values have no variation | Name the client need and change pressure |
Mistakes 4 to 6
| Mistake | Why it is misleading | Better action |
|---|---|---|
| A sender must expose provider administration | The coordinator does not need those operations | Keep the application role narrow |
| A container singleton is thread-safe | Lifetime and concurrent access are separate | Inspect mutable state and execution model |
| Notification failure means reservation failure | Storage may already have succeeded | State the partial-success outcome explicitly |
Mistakes 7 and 8
| Mistake | Why it is misleading | Better action |
|---|---|---|
| One call means exactly-once delivery | Output may fail after partial or remote effects | Describe only the guarantee implemented |
| Old checks passed, so all compatibility is preserved | Construction changed and notification adds effects | Separate API migration from retained domain behaviour |
What You Start With, and the Demonstration
- You start with: Chapter 5’s exit code, this chapter’s requirement and companion listings. Java 17, the plain-Java gate. The container walkthrough is separate from the application.
- Demonstration: compare the Chapter 5 baseline, the coupled notification version and the injected version. Point to every concrete provider choice and explain which belongs at the entry point.
- Guided practice: inject a recorder, accept one reservation, reject an overlapping request and inspect both stored reservations and recorded notifications; then use an output that throws the documented exception.
Independent Work: Steps 1 to 3
- Define the sender role and identify at least two operations it should not contain.
- Move output construction and category-to-policy mapping to the composition root. Require valid collaborators in the constructor.
- Migrate existing caller construction while preserving existing reservation assertions.
Independent Work: Steps 4 to 6
- Demonstrate both console and recording outputs with the same coordinator implementation.
- Verify no notification on rejection, one attempt after acceptance, and the chosen failure outcome without automatic retry.
- Draw separate source-dependency and object-assembly views, labelling what each arrow means. In the source view, check that every “creates” arrow starts at the application class.
Independent Work: Steps 7 to 9
- Run the educational container walkthrough and explain shared state, missing registration and cycle detection.
- Review the change and write a rationale for every new boundary. Retain real review findings.
- On your team’s project, find one environment or provider choice that a class constructs for itself (an output, storage, an external service, configuration). Sketch where it would be assembled instead, using both views.
Expected Output and Completion
- Output: injected notification contract; two implementations; explicit assembly code; migrated and additional checks; the two diagrams; failure-policy explanation; a short rationale; the step 9 sketch.
- Complete when: provider substitution requires no edit to the reservation algorithm; setup failures are distinguishable from reservation rejections; old rule expectations remain valid; expected notification failure preserves the record and reports its outcome accurately.
- If you want more: compare the supplied map with a dedicated policy-resolver in a design sketch. Explain the client benefit before adding another interface.
Review Evidence Worth Inspecting (1 of 2)
| Review concern | Evidence worth inspecting |
|---|---|
| Role is client-specific | Coordinator uses only the sender operation and the domain record |
| Provider choice is external | Concrete output construction is in assembly code, not the coordinator |
| Valid configuration | Null and missing-policy failures occur before request processing |
| Mapping is stable | Editing the caller’s map does not rewire an existing coordinator |
Review Evidence Worth Inspecting (2 of 2)
| Review concern | Evidence worth inspecting |
|---|---|
| Existing behaviour | Migrated setup with unchanged reservation assertions passes |
| Feature effects | Accepted record supplied once; rejection causes no send; expected failure retains state |
| Lifetime | Repeated container resolution shares the same coordinator |
| Claims are bounded | No claim of email delivery, atomic messaging, thread safety or a hosted run |
What the 28 New Checks Establish
- The accepted result, one recorded notification, the exact accepted record, and no notification on conflict, duration rejection or invalid identifiers.
- A controlled expected failure after storage: the suffix, record retention, no automatic retry.
- Two further checks: a sender throwing
IllegalStateExceptionpropagates, with the record already stored. Without them, widening the catch toRuntimeExceptionwould pass every check. - Constructor checks for null dependencies and missing category configuration; a mutable input map changed after construction does not rewire the coordinator.
Do the Checks Detect Faults? (1 of 4)
| Fault | Result |
|---|---|
| Notification sent before the record is stored | Caught: notification failure retains accepted record |
catch (NotificationException) widened to catch (RuntimeException) | Caught: unexpected sender failure is not swallowed |
| Failure suffix text changed | Caught: explicit partial-success result |
| One retry added after a failed send | Caught: one attempt and no automatic retry |
Do the Checks Detect Faults? (3 of 4)
| Fault | Result |
|---|---|
| Null formatter check removed | Caught: null formatter |
| Policy lookup always uses the student policy | Caught: staff confirmation with new limit |
Console checkError() test removed | Caught: console reports expected output failure |
Console prefix Notice: changed | Caught: console implementation uses supplied stream |
Concept Questions
- Why does
NotificationSenderomit the recorder’sreceived()method? - How can constructor injection be present while the intended DIP boundary is still missing?
- What does the composition root know that the coordinator should not know?
- Why can sharing a coordinator through a container change application behaviour even when its methods are unchanged?
Diagnosis Questions
- A coordinator catches a sender failure, deletes the accepted reservation, and returns “Rejected”. The sender may already have printed or delivered a notice. Diagnose the inconsistent outcome and compare it with this chapter’s explicit failure rule.
- A constructor copies a map of policies. A reviewer says this proves that policy behaviour can never change. Explain what the copy protects and what still depends on the policy objects’ contracts.
Guidance: Questions 1 and 2
- The coordinator submits an accepted record; it does not inspect output history. The recorder’s inspection API serves another client, such as check code. Adding it would force the console implementation to invent a history it does not need.
- A constructor taking
ConsoleNotificationSenderuses DI but depends on that concrete provider. A contract copied from a provider SDK keeps provider coupling despite its interface name. Define the application role and inject a conforming implementation.
Guidance: Questions 3 and 4
- The root knows which category maps to which concrete policy, which sender is selected, which stream it uses, and how formatters are constructed. The coordinator knows roles and when to invoke them.
- Two resolutions of one cached coordinator share the same reservations. Two fresh coordinators have separate empty lists and can each accept what looks like the first reservation. The method source is identical; the assembled graph changes behaviour.
Guidance: Questions 5 and 6
- The output may already have occurred, so deleting cannot undo it: a notice for a record that no longer exists. “Rejected” hides the earlier acceptance and may prompt a repeat. This chapter retains the record and reports the output failure. Another policy needs an explicit consistency design, not a catch block pretending to reverse external effects.
- The copy fixes associations between categories and policy references. It is shallow: an externally held policy could still change internally if its implementation permitted it. The stateless policy contract provides the further stability.
Transfer Challenge
A document conversion service validates a request, converts a file and sends a completion notice. A desktop demonstration displays notices in a window; an automated run records them in memory.
- Define a client-specific notification role.
- Identify the assembly boundary and what stays in the conversion workflow.
- Decide what callers observe if conversion succeeds but notification fails.
- Draw a dependency diagram; give two acceptance examples separating conversion state from notification state.
- Synthetic filenames only; no assumed retry queue.
Transfer Guidance
- A
CompletionNotifierrole taking an immutable successful-conversion result. It does not validate, convert or administer accounts. - The workflow decides when conversion has succeeded. The application root selects the desktop or recording notifier.
- Failure rule: keep the converted file; return a result separating conversion success from notification failure.
| Example | Conversion state | Notification state |
|---|---|---|
| Valid input converts | One output produced | One completion result submitted |
| Conversion fails | No success reported | No notice submitted |
| Conversion succeeds, notifier fails | Output retained | Failure reported separately |
What Quiz 1 Covers
- Quiz 1 is held in Week 5, but its stated coverage is Weeks 1 to 4: OO foundations, Agile practices and user stories, Git workflows, and the first SOLID principles.
- Revise Chapters 2 to 5. Do not treat all of Chapter 6 as newly included quiz content.
- Follow the module leader’s confirmed guidance. These pointers add no assessment rules and predict no questions.
Revision Focus (1 of 2)
| Chapter | Revision focus |
|---|---|
| 2 | Encapsulation, responsibilities, cohesion/coupling, composition and inheritance, UML class diagrams, Git history, branches and small reviewed changes |
| 3 | Agile values in decisions, iterative/incremental delivery, Scrum/Kanban distinctions, stories, acceptance criteria and Definition of Done |
Summary
- A useful interface describes a client’s needed role.
- DIP concerns the direction and ownership of dependencies; DI supplies collaborators from outside; a container is one possible assembly mechanism.
- The running example now makes provider and policy choices explicit at its entry boundary and defines the observable result of expected notification failure.
Sources and Further Reading
- Martin: Design Principles and Design Patterns (2000); A Little Architecture (2016).
- Fowler: Inversion of Control Containers and the Dependency Injection Pattern (2004); Inversion of Control (2005).
- Oracle: Java SE 17
MapandPrintStream. - Spring: Introduction to the Spring IoC Container and Beans.
- Martin: Agile Software Development, 2002.
- Full references and links are in the chapter text.