Lecture Slides

Chapter 6: Interfaces, Dependencies, and Composition

SE506: Object Oriented Development and Agile Practices

Dr. Polla Abdulhamid Fattah
Department of Computer Science & Artificial Intelligence
University of Kurdistan Hewlêr (UKH)

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.

Learning Outcomes (1 of 2)

By the end of this chapter, you should be able to:

  1. Design an interface around what a particular client needs.
  2. Explain ISP through a concrete dependency that is unnecessarily broad.
  3. Distinguish DIP, dependency injection and inversion of control.

Learning Outcomes (2 of 2)

  1. Construct a small object graph using constructor injection and an explicit composition root.
  2. Explain the benefits and costs of an interface, including the limits of what it guarantees.

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 the Chapter Introduces

  • The Interface Segregation Principle (ISP).
  • The Dependency Inversion Principle (DIP).
  • Constructor injection and a composition root to assemble the running application.
  • A small container walkthrough that makes object lifetime visible, with no framework installation.

6.1 Why Dependencies Make Change Hard

Begin with a real boundary

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.

6.2 Designing an Interface Around What the Client Needs

Design for the caller’s role

ISP

  • ISP directs attention to clients forced to depend on operations they do not need.
  • The needed role here is small: accept an already confirmed reservation for notification output.
public interface NotificationSender {
    void send(Reservation reservation);
}

A Broad Interface to Avoid

  • An all-purpose EquipmentPlatform interface 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 BorrowingPolicy remains 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 Sender Code

@FunctionalInterface
public interface NotificationSender {
    void send(Reservation reservation);
}

public final class NotificationException extends RuntimeException {
    public NotificationException(String message) { super(message); }
}
  • A normal return is not a promise of email delivery.

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.");
    }
}
  • PrintStream normally records I/O errors rather than throwing, so the sender checks checkError() and translates the expected local error.
  • The error state is sticky: once reported, later notices through the same sender are reported as failed too.

6.3 Who Decides What, and What Happens When Notification Fails

Three decisions, and partial success

Separate Three Decisions

DecisionOwner in this versionReason
Whether the request is acceptableCoordinator and selected borrowing policyDepends on reservation rules and current records
How notification is represented locallySender implementation and formatterDepends on the chosen output
Which concrete objects are usedApplication composition rootDepends 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 NotificationException it 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.

The Failure Outcomes

Request accepted:checks passedStore therecordFormat theconfirmationnotifications.send(reservation)Returns normallythe confirmationNotificationExceptionconfirmation + suffixrecord retained, no retryAny other exceptionpropagates; the recordis already storedone attempt per accepted request

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)

ExampleReservation resultNotification
Eligible available requestExact confirmation; one record storedOne accepted record supplied to the sender
Same-item overlapExisting rejection; records unchangedNo call
Invalid durationExisting rejection; records unchangedNo call

Acceptance Examples Before Refactoring (2 of 2)

ExampleReservation resultNotification
Invalid identifierExisting exception; records unchangedNo call
Expected output failure after acceptanceConfirmation plus failure suffix; record retainedOne attempt; no automatic retry
Repeat of the accepted requestOverlap rejectionNo second attempt
  • These constrain the feature whether assembly is manual or container-based.

6.4 Who Owns the Abstraction?

Dependency inversion

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.

Figure 6.1: Source-Level Dependencies

ReservationApplicationReservationCoordinator«interface»NotificationSender+send(Reservation reservation) voidConsoleNotificationSenderRecordingNotificationSendercreatescreatesnotifiesEvery “creates” arrowstarts at the application.The policy map and theformatter are built there too.

Reading Figure 6.1

  • Solid arrows are associations: the coordinator holds a NotificationSender, a ConfirmationFormatter and a Map<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 ConsoleNotificationSender through 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 NotificationSender assigned new ConsoleNotificationSender(...) inside the coordinator uses an abstraction while keeping concrete construction knowledge. This is the intermediate defect.

Two Different Questions

  • What type of behaviour does the client require? (the role: DIP, ISP)
  • Who supplies the object? (injection)
  • The final design uses the small application contract and passes the implementation from the application root.
  • They support each other but answer different questions.

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 Reservation and LoanPeriod values and uses an in-memory list. Creating a small domain value differs from selecting an environmental output provider.
  • The concrete ConfirmationFormatter is supplied in the constructor, but has no interface: a substitutable formatting role has not been required.
  • Using Java’s Map interface does not itself demonstrate DIP. The decision is that policy mappings are assembled outside the coordinator.

6.5 Handing Collaborators in

Injection and the composition root

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.copyOf produces 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 category switch.

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);
}
  • main chooses 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.

Figure 6.2: The Objects Built at Run Time

Applicationentry pointStudent and staffpolicy mapConsole senderinstanceConfirmationformatterCoordinatorinstancecreatescreatespassed to theconstructor

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 reserve still 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.

A Long Constructor

  • A very long constructor can expose a class with too many responsibilities, but parameter count alone does not prove it.
  • Do not hide ten unrelated services inside a generic context object to shorten the signature.
  • The dependencies would still exist, with less visibility.

6.6 Inversion of Control and Containers

What do they add?

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)

TermQuestion answered in this example
DIPDoes the coordinator depend on the application’s notification role rather than a provider detail?
DIWho supplies the sender, formatter and policy mapping?
IoCWhich component controls assembly or later calls into application code?

Terms and the Question Each Answers (2 of 2)

TermQuestion answered in this example
DI containerWhat mechanism resolves registrations and manages selected object lifetimes?
Service locatorDoes application code fetch collaborators from a registry rather than receive them explicitly?

The Educational Container

  • DemoContainer, in ContainerWalkthrough.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.
  • ReservationCoordinator has 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

  1. Registration stores a provider without running it.
  2. The first coordinator resolution runs its provider, which requests the formatter and sender.
  3. Those providers run; their instances are passed to the constructor.
  4. The coordinator is cached.
  5. 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 javac gate does not manage. The lab uses the educational container, so everything needed is in the chapter and companion.

6.7 What Abstraction Costs

Give each boundary a job

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 Reservation or LoanPeriod.
  • 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 ApplicationContext with arbitrary lookups hides all the coordinator’s real needs. A method calling context.get("mail") is still locating its own collaborator.
  • An interface whose send needs 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.

Concrete Review Questions

  • Can I choose the recorder without editing reservation logic?
  • Can I see the required dependencies at construction?
  • Can a provider-specific change stay at the boundary?
  • Can I explain the state after expected output failure?
  • These reveal more than counting interface declarations.

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.

6.8 Reviewing an Interface That Hides Very Little, with AI

Does the refactoring separate anything?

Ground Rules for the Activity (1 of 2)

For this activity
Your goalJudge whether a generated refactoring really separates configuration and policy from the reservation workflow
You start withThe coupled coordinator from 6.1, the notification requirement and Figure 6.1
What is in boundsEvaluate 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 checkThe coordinator’s source (which concrete classes does it name?), the recorder-based checks, the failure case
How to decideA 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 NotificationSender and type the coordinator field with it. Inside the coordinator constructor, read the provider settings and create ConsoleNotificationSender. Add configure, exportHistory and deleteBorrower to 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 changeVerdictReason
Type the field as NotificationSenderUseful but insufficient aloneIt expresses a role, but concrete construction can remain
Read provider settings inside the coordinatorRejectEnvironmental configuration still changes the workflow’s source
Construct the console sender inside the coordinatorReject for the final designThe caller cannot select a recorder without editing the client

Model Critique (2 of 3)

Claim or changeVerdictReason
Add configure/export/delete to the sender roleReject without a client needThey couple this client to unrelated capabilities
Catch every runtime exception, return ordinary confirmationRejectIt hides programming defects; expected failure becomes indistinguishable from success

Model Critique (3 of 3)

Claim or changeVerdictReason
Inject a global container for future lookupsRejectReal required collaborators become hidden behind a registry
Declare DIP and DI complete because an interface existsRejectContract 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.

Independent Verification

  • Inspect concrete constructor calls.
  • Search the coordinator for provider and container references.
  • Run with the recorder.
  • Observe the failure case.
  • A second model agreeing with the first is not a substitute for those observations.

6.9 Dependency Habits That Mislead

Eight misleading beliefs

Mistakes 1 to 3

MistakeWhy it is misleadingBetter action
An interface-typed field proves DIPConcrete construction or provider-specific types may remainInspect the actual source dependencies
DI means a frameworkOrdinary constructors can receive dependenciesExplain assembly before adding tooling
Every class needs an interfaceMany stable values have no variationName the client need and change pressure

Mistakes 4 to 6

MistakeWhy it is misleadingBetter action
A sender must expose provider administrationThe coordinator does not need those operationsKeep the application role narrow
A container singleton is thread-safeLifetime and concurrent access are separateInspect mutable state and execution model
Notification failure means reservation failureStorage may already have succeededState the partial-success outcome explicitly

Mistakes 7 and 8

MistakeWhy it is misleadingBetter action
One call means exactly-once deliveryOutput may fail after partial or remote effectsDescribe only the guarantee implemented
Old checks passed, so all compatibility is preservedConstruction changed and notification adds effectsSeparate API migration from retained domain behaviour

6.10 Try It Yourself

Assemble a replaceable output

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

  1. Define the sender role and identify at least two operations it should not contain.
  2. Move output construction and category-to-policy mapping to the composition root. Require valid collaborators in the constructor.
  3. Migrate existing caller construction while preserving existing reservation assertions.

Independent Work: Steps 4 to 6

  1. Demonstrate both console and recording outputs with the same coordinator implementation.
  2. Verify no notification on rejection, one attempt after acceptance, and the chosen failure outcome without automatic retry.
  3. 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

  1. Run the educational container walkthrough and explain shared state, missing registration and cycle detection.
  2. Review the change and write a rationale for every new boundary. Retain real review findings.
  3. 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 concernEvidence worth inspecting
Role is client-specificCoordinator uses only the sender operation and the domain record
Provider choice is externalConcrete output construction is in assembly code, not the coordinator
Valid configurationNull and missing-policy failures occur before request processing
Mapping is stableEditing the caller’s map does not rewire an existing coordinator

Review Evidence Worth Inspecting (2 of 2)

Review concernEvidence worth inspecting
Existing behaviourMigrated setup with unchanged reservation assertions passes
Feature effectsAccepted record supplied once; rejection causes no send; expected failure retains state
LifetimeRepeated container resolution shares the same coordinator
Claims are boundedNo 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 IllegalStateException propagates, with the record already stored. Without them, widening the catch to RuntimeException would 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)

FaultResult
Notification sent before the record is storedCaught: notification failure retains accepted record
catch (NotificationException) widened to catch (RuntimeException)Caught: unexpected sender failure is not swallowed
Failure suffix text changedCaught: explicit partial-success result
One retry added after a failed sendCaught: one attempt and no automatic retry

Do the Checks Detect Faults? (2 of 4)

FaultResult
Sender called twice on successCaught: one notification per accepted request
Map.copyOf removedCaught: caller map mutation cannot rewire coordinator
Missing-category check removedCaught: missing staff policy
Null sender check removedCaught: null sender

Do the Checks Detect Faults? (3 of 4)

FaultResult
Null formatter check removedCaught: null formatter
Policy lookup always uses the student policyCaught: staff confirmation with new limit
Console checkError() test removedCaught: console reports expected output failure
Console prefix Notice: changedCaught: console implementation uses supplied stream

Do the Checks Detect Faults? (4 of 4)

FaultResult
Recorder returns its mutable listCaught: inbox snapshot unmodifiable
Factory ignores the supplied senderCaught: one notification per accepted request
  • All fourteen faults caught.
  • The checks do not prove thread safety, durable output or remote delivery.

6.11 Questions to Think with

Concept and diagnosis

Concept Questions

  1. Why does NotificationSender omit the recorder’s received() method?
  2. How can constructor injection be present while the intended DIP boundary is still missing?
  3. What does the composition root know that the coordinator should not know?
  4. Why can sharing a coordinator through a container change application behaviour even when its methods are unchanged?

Diagnosis Questions

  1. 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.
  2. 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

  1. 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.
  2. A constructor taking ConsoleNotificationSender uses 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

  1. 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.
  2. 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

  1. 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.
  2. 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.

6.12 A Different Setting

The transfer challenge

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 CompletionNotifier role 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.
ExampleConversion stateNotification state
Valid input convertsOne output producedOne completion result submitted
Conversion failsNo success reportedNo notice submitted
Conversion succeeds, notifier failsOutput retainedFailure reported separately

6.13 Revising for Quiz 1

Where to look

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)

ChapterRevision focus
2Encapsulation, responsibilities, cohesion/coupling, composition and inheritance, UML class diagrams, Git history, branches and small reviewed changes
3Agile values in decisions, iterative/incremental delivery, Scrum/Kanban distinctions, stories, acceptance criteria and Definition of Done

Revision Focus (2 of 2)

ChapterRevision focus
4Vision and scope, vertical slicing, capacity-aware planning, review workflow and meaningful CI evidence
5SRP, OCP, behavioural contracts, LSP counterexamples and protected refactoring

What to Take Away, and What Comes Next

What to carry forward

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.

Where the Project Stands

  • Local console and recording outputs.
  • Explicit construction and 28 new checks.
  • Existing reservation rules remain covered by their previous expectations, with caller setup migrated.

Next: Chapter 7

  • Test design.
  • Controlled substitutes (test doubles).
  • A complete red-green-refactor cycle.

Sources and Further Reading

  1. Martin: Design Principles and Design Patterns (2000); A Little Architecture (2016).
  2. Fowler: Inversion of Control Containers and the Dependency Injection Pattern (2004); Inversion of Control (2005).
  3. Oracle: Java SE 17 Map and PrintStream.
  4. Spring: Introduction to the Spring IoC Container and Beans.
  5. Martin: Agile Software Development, 2002.
  • Full references and links are in the chapter text.