Chapter 6 Companion: Code, Explanations and Solutions
Code explanations and solutions for Chapter 6 Companion: Code, Explanations and Solutions.
Interfaces, Dependencies and Composition OO Development and Agile Practices
1. Versions and reconstruction
This file accompanies Chapter 6, Interfaces, Dependencies, and Composition. It supplies a complete running Java version, the starting and deliberately coupled coordinator versions, a small educational container and model solutions.
Stage
Purpose
How to use it
Chapter 5 baseline
Policies and formatter, no notification output
Use the complete Chapter 5 after version
Coupled teaching version
Add local notification with direct provider construction
In a copy of Chapter 5, replace the coordinator with Section 2.2 and add the three indicated sender files
Chapter 6 result
Supply notification, formatting and policy mapping from outside
Reconstruct the complete after directory from Sections 3–5
Adding notification is a feature change. The next structural step separates construction from the request workflow. Removing the no-argument coordinator constructor is an explicit source API migration, not transparent compatibility.
1.1 Reconstruct the complete result
Every listing beginning with after/ belongs in one directory named after. Create its intermediate directories and copy only the content of each fence. Do not add the two Section 2 coordinator alternatives to that directory. Their paths beginning with before/ and coupled/ identify different teaching versions.
Section 3 contains every application source file. Section 4 contains all four check runners. Section 5 contains the build script, ignore file and README. Section 6 contains the optional container program, which is excluded from the normal application gate.
The result uses Java 17, Bash and the standard library. It has no external dependencies, Maven configuration or JUnit dependency at this version. The existing unnamed-package layout remains. A framework container is not required.
The gate should report the original 34 checks, five acceptance groups, 44 Chapter 5 checks and:
All 28 Chapter 6 checks passed.
BUILD AND CHECKS PASSED
The new application prints:
Notice: Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22).
Result: Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22).
The two lines have different roles. The sender prints a local notice; the application’s main method prints the returned reservation result. This does not mean the sender was invoked twice.
The Chapter 4 and Chapter 5 demonstrations retain their earlier output because they now use a recorder for notification. Their construction expressions have changed, but their request sequences and expected reservation results have not. No remote service is contacted by any supplied example.
2. Starting and deliberately coupled coordinator versions
These two listings are alternatives to the completed coordinator. For either one, use the complete Chapter 5 version’s other files and gate. Do not mix the Chapter 6 composition root or migrated check runners into these older-constructor versions.
2.1 Starting coordinator
This is exactly the Chapter 5 coordinator. It formats a return value and does not call a notification sender.
importjava.time.LocalDate;importjava.util.ArrayList;importjava.util.List;importjava.util.Objects;publicfinalclassReservationCoordinator{privatefinalList<Reservation>reservations=newArrayList<>();privatefinalBorrowingPolicystudentPolicy=newReservationRules();privatefinalBorrowingPolicystaffPolicy=newStaffReservationRules();privatefinalConfirmationFormatterformatter=newConfirmationFormatter();publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intrequestedDays){returnreserve(borrowerId,itemId,start,requestedDays,BorrowerCategory.STUDENT);}publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intrequestedDays,BorrowerCategorycategory){Objects.requireNonNull(category,"category");if(borrowerId==null||borrowerId.isBlank()||itemId==null||itemId.isBlank()){thrownewIllegalArgumentException("Identifiers must not be blank.");}Objects.requireNonNull(start,"start");intactiveCount=0;for(Reservationexisting:reservations){if(existing.borrowerId().equals(borrowerId)){activeCount++;}}if(!policyFor(category).mayRequest(activeCount,requestedDays)){return"Rejected: borrower limit or duration.";}LoanPeriodrequested=newLoanPeriod(start,start.plusDays(requestedDays));for(Reservationexisting:reservations){if(existing.itemId().equals(itemId)&&existing.period().overlaps(requested)){return"Rejected: item unavailable.";}}Reservationreservation=newReservation("R"+(reservations.size()+1),borrowerId,itemId,requested);reservations.add(reservation);returnformatter.format(reservation);}privateBorrowingPolicypolicyFor(BorrowerCategorycategory){returnswitch(category){caseSTUDENT->studentPolicy;caseSTAFF->staffPolicy;};}publicList<Reservation>reservations(){returnList.copyOf(reservations);}}
2.2 Coupled notification feature
In a separate copy of Chapter 5, use this coordinator and add NotificationSender.java, NotificationException.java and ConsoleNotificationSender.java from Section 3. Its original no-argument construction remains. It chooses console output directly, so accepted requests now print notices while the old checking assertions still examine reservation results.
importjava.time.LocalDate;importjava.util.ArrayList;importjava.util.List;importjava.util.Objects;publicfinalclassReservationCoordinator{privatefinalList<Reservation>reservations=newArrayList<>();privatefinalBorrowingPolicystudentPolicy=newReservationRules();privatefinalBorrowingPolicystaffPolicy=newStaffReservationRules();privatefinalConfirmationFormatterformatter=newConfirmationFormatter();privatefinalNotificationSendernotifications=newConsoleNotificationSender(System.out,newConfirmationFormatter());publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intrequestedDays){returnreserve(borrowerId,itemId,start,requestedDays,BorrowerCategory.STUDENT);}publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intrequestedDays,BorrowerCategorycategory){Objects.requireNonNull(category,"category");if(borrowerId==null||borrowerId.isBlank()||itemId==null||itemId.isBlank()){thrownewIllegalArgumentException("Identifiers must not be blank.");}Objects.requireNonNull(start,"start");intactiveCount=0;for(Reservationexisting:reservations){if(existing.borrowerId().equals(borrowerId)){activeCount++;}}if(!policyFor(category).mayRequest(activeCount,requestedDays)){return"Rejected: borrower limit or duration.";}LoanPeriodrequested=newLoanPeriod(start,start.plusDays(requestedDays));for(Reservationexisting:reservations){if(existing.itemId().equals(itemId)&&existing.period().overlaps(requested)){return"Rejected: item unavailable.";}}Reservationreservation=newReservation("R"+(reservations.size()+1),borrowerId,itemId,requested);reservations.add(reservation);Stringconfirmation=formatter.format(reservation);try{notifications.send(reservation);}catch(NotificationExceptionfailure){returnconfirmation+" Notification failed; reservation retained.";}returnconfirmation;}privateBorrowingPolicypolicyFor(BorrowerCategorycategory){returnswitch(category){caseSTUDENT->studentPolicy;caseSTAFF->staffPolicy;};}publicList<Reservation>reservations(){returnList.copyOf(reservations);}}
The field is typed as NotificationSender, but the initializer names ConsoleNotificationSender, System.out and a concrete formatter. The workflow therefore still selects its own output environment. This newly introduced teaching defect is not a claim about the previous chapter.
The coupled version already uses the explicit retain-and-report rule for expected notification failure. The final structural step preserves that feature policy while changing how collaborators arrive. A recording configuration intentionally changes the output destination; it does not change the acceptance algorithm.
3. Complete application source for after
The first seven files retain Chapter 5’s domain and formatting behaviour. The remaining files establish notification and explicit composition. Paths in this section are the actual files to create.
3.1 after/src/main/java/BorrowerCategory.java
publicenumBorrowerCategory{STUDENT,STAFF}
3.2 after/src/main/java/BorrowingPolicy.java
/**
* A deterministic, side-effect-free borrowing decision for every pair of ints.
* Return false for negative counts or nonpositive durations; do not throw for them.
* Return false for requests outside this implementation's documented limits.
* A true result concerns count/duration only, not item availability or identity.
*/publicinterfaceBorrowingPolicy{booleanmayRequest(intactiveReservations,intrequestedDays);}
/** Illustrative teaching policy, not an approved institutional rule. */publicfinalclassStaffReservationRulesimplementsBorrowingPolicy{@OverridepublicbooleanmayRequest(intactiveReservations,intrequestedDays){returnactiveReservations>=0&&activeReservations<5&&requestedDays>=1&&requestedDays<=14;}}
3.5 after/src/main/java/LoanPeriod.java
importjava.time.LocalDate;importjava.util.Objects;publicfinalclassLoanPeriod{privatefinalLocalDatestart;privatefinalLocalDateendExclusive;publicLoanPeriod(LocalDatestart,LocalDateendExclusive){this.start=Objects.requireNonNull(start,"start");this.endExclusive=Objects.requireNonNull(endExclusive,"endExclusive");if(!start.isBefore(endExclusive)){thrownewIllegalArgumentException("End must be after start.");}}publicLocalDatestart(){returnstart;}publicLocalDateendExclusive(){returnendExclusive;}publicbooleanoverlaps(LoanPeriodother){Objects.requireNonNull(other,"other");returnstart.isBefore(other.endExclusive)&&other.start.isBefore(endExclusive);}}
3.6 after/src/main/java/Reservation.java
importjava.util.Objects;publicfinalclassReservation{privatefinalStringid;privatefinalStringborrowerId;privatefinalStringitemId;privatefinalLoanPeriodperiod;publicReservation(Stringid,StringborrowerId,StringitemId,LoanPeriodperiod){this.id=requireText(id);this.borrowerId=requireText(borrowerId);this.itemId=requireText(itemId);this.period=Objects.requireNonNull(period,"period");}privatestaticStringrequireText(Stringvalue){if(value==null||value.isBlank()){thrownewIllegalArgumentException("Identifiers must not be blank.");}returnvalue;}publicStringid(){returnid;}publicStringborrowerId(){returnborrowerId;}publicStringitemId(){returnitemId;}publicLoanPeriodperiod(){returnperiod;}}
importjava.util.Objects;publicfinalclassConfirmationFormatter{publicStringformat(Reservationreservation){Objects.requireNonNull(reservation,"reservation");return"Confirmed: "+reservation.id()+" for "+reservation.borrowerId()+" on "+reservation.itemId()+" ["+reservation.period().start()+", "+reservation.period().endExclusive()+").";}}
3.8 after/src/main/java/NotificationSender.java
/**
* Accept one non-null confirmed reservation for this implementation's local output.
* Do not modify the reservation or coordinator. Report expected output failure with
* NotificationException. Normal return is not a promise of email delivery.
*/@FunctionalInterfacepublicinterfaceNotificationSender{voidsend(Reservationreservation);}
importjava.time.LocalDate;importjava.util.ArrayList;importjava.util.List;importjava.util.Objects;importjava.util.Map;publicfinalclassReservationCoordinator{privatefinalList<Reservation>reservations=newArrayList<>();privatefinalMap<BorrowerCategory,BorrowingPolicy>policies;privatefinalConfirmationFormatterformatter;privatefinalNotificationSendernotifications;publicReservationCoordinator(Map<BorrowerCategory,BorrowingPolicy>policies,ConfirmationFormatterformatter,NotificationSendernotifications){this.policies=Map.copyOf(Objects.requireNonNull(policies,"policies"));for(BorrowerCategorycategory:BorrowerCategory.values()){if(!this.policies.containsKey(category)){thrownewIllegalArgumentException("Missing policy: "+category);}}this.formatter=Objects.requireNonNull(formatter,"formatter");this.notifications=Objects.requireNonNull(notifications,"notifications");}publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intrequestedDays){returnreserve(borrowerId,itemId,start,requestedDays,BorrowerCategory.STUDENT);}publicStringreserve(StringborrowerId,StringitemId,LocalDatestart,intrequestedDays,BorrowerCategorycategory){Objects.requireNonNull(category,"category");if(borrowerId==null||borrowerId.isBlank()||itemId==null||itemId.isBlank()){thrownewIllegalArgumentException("Identifiers must not be blank.");}Objects.requireNonNull(start,"start");intactiveCount=0;for(Reservationexisting:reservations){if(existing.borrowerId().equals(borrowerId)){activeCount++;}}if(!policies.get(category).mayRequest(activeCount,requestedDays)){return"Rejected: borrower limit or duration.";}LoanPeriodrequested=newLoanPeriod(start,start.plusDays(requestedDays));for(Reservationexisting:reservations){if(existing.itemId().equals(itemId)&&existing.period().overlaps(requested)){return"Rejected: item unavailable.";}}Reservationreservation=newReservation("R"+(reservations.size()+1),borrowerId,itemId,requested);reservations.add(reservation);Stringconfirmation=formatter.format(reservation);try{notifications.send(reservation);}catch(NotificationExceptionfailure){returnconfirmation+" Notification failed; reservation retained.";}returnconfirmation;}publicList<Reservation>reservations(){returnList.copyOf(reservations);}}
ReservationApplication is the assembly boundary. Its create method supplies a map containing student and staff policies, a concrete formatter and the sender selected by its caller. Its main method chooses a console sender and stream. The coordinator knows the sender contract and policy map, not those provider choices.
The coordinator’s constructor rejects missing dependencies and incomplete category mappings. Map.copyOf stops later changes to the caller’s map from changing this mapping. It does not deep-copy the policy instances; their existing stateless contract still matters.
The original reserve overload continues to delegate with STUDENT. The explicit-category overload still trusts its supplied category, and all records for the borrower contribute to the active count. No identity verification has been added.
After acceptance the coordinator stores the record, formats its return value and makes one sender call. Expected notification failure adds the explicit suffix while retaining the record. There is no retry, rollback or queued work. An unexpected runtime failure is not swallowed; it can propagate after storage. Do not describe the operation as atomic across output and state.
RecordingNotificationSender holds immutable reservation objects in its own local list and exposes an unmodifiable snapshot. ConsoleNotificationSender writes to the supplied PrintStream and checks its error flag. It does not close the stream because ownership remains with the caller. Checking the stream can detect a local error, but cannot promise that no characters were written before failure.
4. Complete checking programs
The first three runners have exactly the previous assertions and request sequences. Their only source changes replace new ReservationCoordinator() with ReservationApplication.create(new RecordingNotificationSender()). That setup migration is necessary because the no-argument constructor no longer exists.
The fourth runner exercises notification and assembly behaviour. The hand-written failing sender is a controlled collaborator for this exercise; the full test-double taxonomy belongs in Chapter 7.
4.1 after/src/test/java/Chapter02Checks.java
importjava.time.DateTimeException;importjava.time.LocalDate;importjava.util.List;publicclassChapter02Checks{privatestaticintchecks;publicstaticvoidmain(String[]args){LocalDateday=LocalDate.of(2026,10,20);LoanPeriodfirst=newLoanPeriod(day,day.plusDays(2));LoanPeriodoverlapping=newLoanPeriod(day.plusDays(1),day.plusDays(3));LoanPeriodadjacent=newLoanPeriod(day.plusDays(2),day.plusDays(3));check(first.overlaps(overlapping),"overlap detected");check(overlapping.overlaps(first),"overlap is symmetric");check(!first.overlaps(adjacent),"adjacent periods allowed");check(!adjacent.overlaps(first),"adjacency symmetric");check(first.overlaps(newLoanPeriod(day,day.plusDays(2))),"equal periods overlap");check(first.overlaps(newLoanPeriod(day.minusDays(1),day.plusDays(4))),"containment overlaps");expect(IllegalArgumentException.class,()->newLoanPeriod(day,day),"zero-length period");expect(IllegalArgumentException.class,()->newLoanPeriod(day,day.minusDays(1)),"reversed period");expect(NullPointerException.class,()->newLoanPeriod(null,day),"null start");expect(NullPointerException.class,()->newLoanPeriod(day,null),"null end");ReservationCoordinatorc=ReservationApplication.create(newRecordingNotificationSender());check(c.reserve("B1","CAM1",day,2).equals("Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22)."),"exact first confirmation");check(c.reservations().size()==1,"one success stored");check(c.reserve("B2","CAM1",day.plusDays(1),1).equals("Rejected: item unavailable."),"overlapping same item rejected");check(c.reservations().size()==1,"conflict stores nothing");check(c.reserve("B2","CAM1",day.plusDays(2),1).equals("Confirmed: R2 for B2 on CAM1 [2026-10-22, 2026-10-23)."),"adjacent request and identifier after rejection");check(c.reserve("B1","MIC1",day,7).equals("Confirmed: R3 for B1 on MIC1 [2026-10-20, 2026-10-27)."),"different item and seven-day limit allowed");check(c.reserve("B1","TRI1",day,1).startsWith("Confirmed:"),"third active reservation allowed");check(c.reserve("B1","CAM2",day,1).equals("Rejected: borrower limit or duration."),"fourth rejected");check(c.reservations().size()==4,"borrower-limit rejection stores nothing");List<Reservation>snapshot=c.reservations();expect(UnsupportedOperationException.class,snapshot::clear,"snapshot unmodifiable");check(c.reservations().size()==4,"failed clear preserves coordinator state");check(c.reserve("B3","CAM2",day,1).startsWith("Confirmed:"),"another borrower unaffected by B1 limit");check(snapshot.size()==4&&c.reservations().size()==5,"old snapshot does not track later additions");check(snapshot.get(0).period().start().equals(day),"stored period preserved");ReservationCoordinatorinvalid=ReservationApplication.create(newRecordingNotificationSender());check(invalid.reserve("B1","CAM1",day,0).equals("Rejected: borrower limit or duration."),"zero duration rejected");check(invalid.reserve("B1","CAM1",day,-1).equals("Rejected: borrower limit or duration."),"negative duration rejected");check(invalid.reserve("B1","CAM1",day,8).equals("Rejected: borrower limit or duration."),"eight-day duration rejected");expect(IllegalArgumentException.class,()->invalid.reserve(" ","CAM1",day,1),"blank borrower");expect(IllegalArgumentException.class,()->invalid.reserve("B1",null,day,1),"null item");expect(NullPointerException.class,()->invalid.reserve("B1","CAM1",null,1),"null request date");expect(DateTimeException.class,()->invalid.reserve("B1","CAM1",LocalDate.MAX,1),"unrepresentable end date");check(invalid.reservations().isEmpty(),"invalid requests store nothing");expect(IllegalArgumentException.class,()->newReservation("","B1","CAM1",first),"blank reservation identifier");expect(NullPointerException.class,()->newReservation("R1","B1","CAM1",null),"null reservation period");System.out.println("All "+checks+" chapter checks passed.");}privatestaticvoidcheck(booleancondition,Stringlabel){if(!condition){thrownewAssertionError(label);}checks++;}privatestaticvoidexpect(Class<?extendsThrowable>type,Runnableaction,Stringlabel){try{action.run();}catch(Throwableerror){check(type.isInstance(error),label+": wrong exception "+error);return;}thrownewAssertionError(label+": expected "+type.getSimpleName());}}
importjava.time.LocalDate;publicclassChapter03AcceptanceExamples{publicstaticvoidmain(String[]args){LocalDateday=LocalDate.of(2026,10,20);ReservationCoordinatorcoordinator=ReservationApplication.create(newRecordingNotificationSender());// AC-01: an eligible request is stored once and confirmed.Stringaccepted=coordinator.reserve("B1","CAM1",day,2);require(accepted.equals("Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22)."),"AC-01 confirmation");require(coordinator.reservations().size()==1,"AC-01 one record");System.out.println("AC-01 passed");// AC-02: an overlapping request for the same item changes no records.Stringconflict=coordinator.reserve("B2","CAM1",day.plusDays(1),1);require(conflict.equals("Rejected: item unavailable."),"AC-02 rejection");require(coordinator.reservations().size()==1,"AC-02 unchanged count");require(coordinator.reservations().get(0).borrowerId().equals("B1"),"AC-02 original borrower preserved");System.out.println("AC-02 passed");// AC-03: a booking may start at an existing exclusive end,// or end exactly where an existing booking starts.Stringadjacent=coordinator.reserve("B2","CAM1",day.plusDays(2),1);require(adjacent.equals("Confirmed: R2 for B2 on CAM1 [2026-10-22, 2026-10-23)."),"AC-03 adjacent confirmation");require(coordinator.reservations().size()==2,"AC-03 second record");Stringbefore=coordinator.reserve("B3","CAM1",day.minusDays(1),1);require(before.equals("Confirmed: R3 for B3 on CAM1 [2026-10-19, 2026-10-20)."),"AC-03 request ending at an existing start");require(coordinator.reservations().size()==3,"AC-03 third record");System.out.println("AC-03 passed");// AC-04: count limits span items, not just one equipment item.ReservationCoordinatorlimits=ReservationApplication.create(newRecordingNotificationSender());for(Stringitem:newString[]{"CAM1","MIC1","TRI1"}){require(limits.reserve("B1",item,day,1).startsWith("Confirmed:"),"AC-04 establish three valid reservations");}require(limits.reserve("B1","CAM2",day,1).equals("Rejected: borrower limit or duration."),"AC-04 fourth rejected");require(limits.reservations().size()==3,"AC-04 unchanged count");System.out.println("AC-04 passed");// AC-05: one and seven days are allowed; zero, negative and eight are not.ReservationCoordinatordurations=ReservationApplication.create(newRecordingNotificationSender());require(durations.reserve("B1","CAM1",day,1).startsWith("Confirmed:"),"AC-05 one day");require(durations.reserve("B1","MIC1",day,7).startsWith("Confirmed:"),"AC-05 seven days");for(intdays:newint[]{0,-1,8}){require(durations.reserve("B1","TRI1",day,days).equals("Rejected: borrower limit or duration."),"AC-05 invalid duration");require(durations.reservations().size()==2,"AC-05 unchanged count");}System.out.println("AC-05 passed");System.out.println("All 5 acceptance examples passed.");}privatestaticvoidrequire(booleancondition,Stringdescription){if(!condition){thrownewAssertionError(description);}}}
4.3 after/src/test/java/Chapter05Checks.java
importjava.time.LocalDate;importjava.util.List;publicfinalclassChapter05Checks{privatestaticintchecks;publicstaticvoidmain(String[]args){BorrowingPolicystudent=newReservationRules();BorrowingPolicystaff=newStaffReservationRules();sharedPolicyContract(student);sharedPolicyContract(staff);check(student.mayRequest(2,7),"student upper accepted boundaries");check(!student.mayRequest(3,7),"student fourth rejected");check(!student.mayRequest(0,8),"student eight days rejected");check(staff.mayRequest(4,14),"staff upper accepted boundaries");check(!staff.mayRequest(5,14),"staff sixth rejected");check(!staff.mayRequest(0,15),"staff fifteen days rejected");LocalDateday=LocalDate.of(2026,10,20);ReservationCoordinatorc=ReservationApplication.create(newRecordingNotificationSender());check(c.reserve("S1","CAM1",day,14,BorrowerCategory.STAFF).equals("Confirmed: R1 for S1 on CAM1 [2026-10-20, 2026-11-03)."),"staff confirmation with new limit");List<Reservation>snapshot=c.reservations();check(c.reserve("B1","CAM1",day.plusDays(1),1).equals("Rejected: item unavailable."),"student conflicts with staff booking");check(c.reservations().equals(snapshot),"conflict preserves exact records");check(c.reserve("B1","CAM1",day.plusDays(14),1).equals("Confirmed: R2 for B1 on CAM1 [2026-11-03, 2026-11-04)."),"cross-category adjacency and next identifier");List<Reservation>beforeInvalid=c.reservations();check(c.reserve("S1","MIC1",day,15,BorrowerCategory.STAFF).equals("Rejected: borrower limit or duration."),"staff invalid duration result");check(c.reservations().equals(beforeInvalid),"invalid staff request preserves records");expect(NullPointerException.class,()->c.reserve("S1","MIC1",day,1,null),"null category");check(c.reservations().equals(beforeInvalid),"null category preserves records");ReservationCoordinatorlimits=ReservationApplication.create(newRecordingNotificationSender());for(inti=1;i<=5;i++){check(limits.reserve("S2","ITEM"+i,day,1,BorrowerCategory.STAFF).startsWith("Confirmed:"),"staff reservation "+i);}List<Reservation>five=limits.reservations();check(limits.reserve("S2","ITEM6",day,1,BorrowerCategory.STAFF).equals("Rejected: borrower limit or duration."),"staff sixth rejected in coordinator");check(limits.reservations().equals(five),"staff limit preserves records");ReservationCoordinatordefaultStudent=ReservationApplication.create(newRecordingNotificationSender());ReservationCoordinatorexplicitStudent=ReservationApplication.create(newRecordingNotificationSender());check(defaultStudent.reserve("B1","MIC1",day,7).equals(explicitStudent.reserve("B1","MIC1",day,7,BorrowerCategory.STUDENT)),"default and explicit student results agree");check(defaultStudent.reserve("B2","TRI1",day,8).equals("Rejected: borrower limit or duration."),"default remains student");// Counts belong to a borrower ID across all records, not separate category buckets.ReservationCoordinatormixedFixture=ReservationApplication.create(newRecordingNotificationSender());for(inti=1;i<=3;i++){mixedFixture.reserve("X1","ITEM"+i,day,1);}check(mixedFixture.reserve("X1","ITEM4",day,1,BorrowerCategory.STAFF).startsWith("Confirmed:"),"supplied staff policy uses existing count three");check(mixedFixture.reserve("X1","ITEM5",day,1,BorrowerCategory.STAFF).startsWith("Confirmed:"),"supplied staff policy uses existing count four");check(mixedFixture.reserve("X1","ITEM6",day,1,BorrowerCategory.STAFF).equals("Rejected: borrower limit or duration."),"counts not reset by category selection");System.out.println("All "+checks+" Chapter 5 checks passed.");}privatestaticvoidsharedPolicyContract(BorrowingPolicypolicy){check(!policy.mayRequest(-1,1),"negative count returns false");check(!policy.mayRequest(Integer.MIN_VALUE,1),"minimum count returns false");check(!policy.mayRequest(0,0),"zero duration returns false");check(!policy.mayRequest(0,-1),"negative duration returns false");check(!policy.mayRequest(0,Integer.MIN_VALUE),"minimum duration returns false");check(policy.mayRequest(0,1),"documented one-day request allowed");check(!policy.mayRequest(Integer.MAX_VALUE,1),"above documented count limits");check(!policy.mayRequest(0,Integer.MAX_VALUE),"above documented duration limits");booleanfirst=policy.mayRequest(1,2);policy.mayRequest(0,1);check(policy.mayRequest(1,2)==first,"repeat decision unchanged by intervening call");}privatestaticvoidcheck(booleancondition,Stringlabel){if(!condition)thrownewAssertionError(label);checks++;}privatestaticvoidexpect(Class<?extendsThrowable>type,Runnableaction,Stringlabel){try{action.run();}catch(Throwableerror){check(type.isInstance(error),label+": wrong exception "+error);return;}thrownewAssertionError(label+": expected "+type.getSimpleName());}}
4.4 after/src/test/java/Chapter06Checks.java
importjava.io.ByteArrayOutputStream;importjava.io.OutputStream;importjava.io.PrintStream;importjava.io.IOException;importjava.nio.charset.StandardCharsets;importjava.time.LocalDate;importjava.util.EnumMap;importjava.util.List;importjava.util.Map;publicfinalclassChapter06Checks{privatestaticintchecks;publicstaticvoidmain(String[]args){LocalDateday=LocalDate.of(2026,10,20);RecordingNotificationSenderinbox=newRecordingNotificationSender();ReservationCoordinatorc=ReservationApplication.create(inbox);Stringfirst=c.reserve("B1","CAM1",day,2);check(first.equals("Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22)."),"accepted return unchanged");check(inbox.received().size()==1,"one notification per accepted request");check(inbox.received().get(0)==c.reservations().get(0),"accepted record supplied");List<Reservation>oldInbox=inbox.received();expect(UnsupportedOperationException.class,oldInbox::clear,"inbox snapshot unmodifiable");check(c.reserve("B2","CAM1",day.plusDays(1),1).equals("Rejected: item unavailable."),"conflict result preserved");check(inbox.received().size()==1,"conflict sends nothing");c.reserve("B1","MIC1",day,8);check(inbox.received().size()==1,"duration rejection sends nothing");expect(IllegalArgumentException.class,()->c.reserve("","CAM1",day,1),"bad ID");check(inbox.received().size()==1,"invalid argument sends nothing");c.reserve("B2","CAM1",day.plusDays(2),1);check(inbox.received().size()==2,"adjacent acceptance sends once");check(oldInbox.size()==1,"earlier inbox snapshot stable");FailingSenderfailure=newFailingSender();ReservationCoordinatorfailed=ReservationApplication.create(failure);StringfailureResult=failed.reserve("B1","CAM1",day,2);check(failureResult.equals(first+" Notification failed; reservation retained."),"explicit partial-success result");check(failed.reservations().size()==1,"notification failure retains accepted record");check(failure.calls==1,"one attempt and no automatic retry");check(failed.reserve("B1","CAM1",day,2).equals("Rejected: item unavailable."),"retrying reservation is still a conflict");check(failure.calls==1,"conflicting repeat does not attempt notification");ReservationCoordinatorunexpected=ReservationApplication.create(newBrokenSender());expect(IllegalStateException.class,()->unexpected.reserve("B1","CAM1",day,2),"unexpected sender failure is not swallowed");check(unexpected.reservations().size()==1,"record is stored before the unexpected failure");Map<BorrowerCategory,BorrowingPolicy>configured=policies();expect(NullPointerException.class,()->newReservationCoordinator(null,newConfirmationFormatter(),inbox),"null policy map");expect(IllegalArgumentException.class,()->newReservationCoordinator(Map.of(BorrowerCategory.STUDENT,newReservationRules()),newConfirmationFormatter(),inbox),"missing staff policy");expect(NullPointerException.class,()->newReservationCoordinator(configured,null,inbox),"null formatter");expect(NullPointerException.class,()->newReservationCoordinator(configured,newConfirmationFormatter(),null),"null sender");Map<BorrowerCategory,BorrowingPolicy>mutable=newEnumMap<>(configured);ReservationCoordinatorcopied=newReservationCoordinator(mutable,newConfirmationFormatter(),newRecordingNotificationSender());mutable.put(BorrowerCategory.STUDENT,(count,days)->false);check(copied.reserve("B1","CAM1",day,1).startsWith("Confirmed:"),"caller map mutation cannot rewire coordinator");Map<BorrowerCategory,BorrowingPolicy>restrictive=newEnumMap<>(configured);restrictive.put(BorrowerCategory.STUDENT,(count,days)->false);RecordingNotificationSenderrestrictedInbox=newRecordingNotificationSender();ReservationCoordinatorrestricted=newReservationCoordinator(restrictive,newConfirmationFormatter(),restrictedInbox);check(restricted.reserve("B1","CAM1",day,1).equals("Rejected: borrower limit or duration."),"injected policy is used");check(restrictedInbox.received().isEmpty(),"injected rejection sends nothing");ByteArrayOutputStreambytes=newByteArrayOutputStream();ConsoleNotificationSenderconsole=newConsoleNotificationSender(newPrintStream(bytes,true,StandardCharsets.UTF_8),newConfirmationFormatter());console.send(c.reservations().get(0));check(bytes.toString(StandardCharsets.UTF_8).equals("Notice: "+first+System.lineSeparator()),"console implementation uses supplied stream");PrintStreambrokenOutput=newPrintStream(newOutputStream(){@Overridepublicvoidwrite(intvalue)throwsIOException{thrownewIOException("Expected output failure");}});ConsoleNotificationSenderbrokenConsole=newConsoleNotificationSender(brokenOutput,newConfirmationFormatter());expect(NotificationException.class,()->brokenConsole.send(c.reservations().get(0)),"console reports expected output failure");expect(NullPointerException.class,()->inbox.send(null),"null notice rejected");System.out.println("All "+checks+" Chapter 6 checks passed.");}privatestaticMap<BorrowerCategory,BorrowingPolicy>policies(){returnMap.of(BorrowerCategory.STUDENT,newReservationRules(),BorrowerCategory.STAFF,newStaffReservationRules());}privatestaticfinalclassFailingSenderimplementsNotificationSender{privateintcalls;@Overridepublicvoidsend(Reservationreservation){calls++;thrownewNotificationException("Expected teaching failure");}}// A programming defect, not an expected output failure: it must not be swallowed.privatestaticfinalclassBrokenSenderimplementsNotificationSender{@Overridepublicvoidsend(Reservationreservation){thrownewIllegalStateException("Unexpected programming failure");}}privatestaticvoidcheck(booleancondition,Stringlabel){if(!condition)thrownewAssertionError(label);checks++;}privatestaticvoidexpect(Class<?extendsThrowable>type,Runnableaction,Stringlabel){try{action.run();}catch(Throwableerror){check(type.isInstance(error),label+": wrong exception "+error);return;}thrownewAssertionError(label+": expected "+type.getSimpleName());}}
4.5 What the 28 new checks establish
The runner checks the accepted result, one recorded notification, the exact accepted record supplied to it, recorder snapshot properties and no notification on conflict, duration rejection or invalid identifiers. It then observes a controlled expected failure after storage, the explicit suffix, record retention and absence of automatic retry. Repeating the conflicting reservation does not trigger another sender call.
Two further checks cover the claim that unexpected programming failures are not swallowed. A sender that throws IllegalStateException makes the reservation call propagate that exception, and the record is already stored when it does. Without them, replacing catch (NotificationException ...) with catch (RuntimeException ...) would pass every check.
Constructor checks exercise null dependencies and missing category configuration. A mutable input map is changed after construction to show that the coordinator’s mapping is insulated from that change. A deliberately restrictive injected student policy shows that the supplied policy is actually used. This is a legal broader-interface policy for the test, not the production student mapping.
The console output is captured in a local byte stream, so it can be compared without contacting any external service. A deliberately broken stream demonstrates translation into NotificationException. Java PrintStream normally stores I/O errors internally; checking only for a thrown IOException around println would miss that mechanism.
The checks do not prove thread safety, durable output or remote delivery. The old assertions preserve evidence for existing reservation rules; the new observations cover the new output feature. Passing counts are evidence about these executed cases, not a measure of design quality.
5. Build and repository files
5.1 after/scripts/check.sh
#!/usr/bin/env bash
set -euo pipefail
cd"$(dirname "${BASH_SOURCE[0]}")/.."mkdir -p build/classes
# Remove only generated files from this starter's output directory.find build/classes -type f -name '*.class' -delete
java -version
"${CHAPTER_JAVAC:-javac}" --release 17 -encoding UTF-8 -d build/classes \
src/main/java/*.java src/test/java/*.java
java -cp build/classes Chapter02Checks
java -cp build/classes Chapter03AcceptanceExamples
java -cp build/classes Chapter05Checks
java -cp build/classes Chapter06Checks
echo"BUILD AND CHECKS PASSED"
5.2 after/.gitignore
/build/
*.class
5.2a after/.gitattributes
*.sh text eol=lf
This is the Chapter 4 file that keeps shell scripts in LF form when Git on Windows converts line endings. Without it, a CRLF copy of scripts/check.sh fails under WSL or Linux bash.
5.3 after/README.md
# Campus Equipment Lending: Chapter 6
Use a Java 17 JDK and Bash. From this directory:
```bash
bash scripts/check.sh
java -cp build/classes ReservationApplication
```The gate runs 34 Chapter 2 checks, five Chapter 3 acceptance groups,
44 Chapter 5 checks and 28 Chapter 6 checks. It must return nonzero on failure.
The application prints one local console notice and its reservation result.
It sends no email and makes no network calls.
The no-argument ReservationCoordinator constructor has been removed.
Use ReservationApplication.create(sender), or supply the policy map,
formatter and sender directly. Existing reserve overloads keep their
student/default and explicit-category meanings.
Expected notification failure retains the accepted reservation and appends
" Notification failed; reservation retained." to the confirmation.
No retry queue, remote delivery guarantee or transaction is implemented.
All records remain active, in memory and sequentially accessed.
Category comes from a trusted fixture, not an authoritative directory.
Availability enquiry, cancellation and loan returns remain unimplemented.
Optional educational container, after a successful gate:
```bash
javac --release 17 -cp build/classes -d build/classes examples/container/ContainerWalkthrough.java
java -cp build/classes ContainerWalkthrough
```The next gate clears generated class files; compile the optional example
again if needed. The container is for teaching, not production use.
The Chapter 4 hosted workflow can still call bash scripts/check.sh when
this checkpoint is the repository root. A local pass is not a hosted run.
The Chapter 4 hosted workflow can keep invoking bash scripts/check.sh when the completed version is the repository root. If the team stores after as a subdirectory, change the invoked path deliberately. The workflow template’s presence is not evidence of hosted execution.
The gate deletes generated class files, compiles all direct .java children in the application/check source directories and runs four checking programs. Its final success marker is only reached if each command succeeds. Optional examples are compiled separately because they are teaching programmes outside the running application.
6. Educational container walkthrough
This is a small isolated illustration, not a recommended framework. It uses typed provider functions, caches one instance per registered type, and identifies missing bindings and cycles. It does not implement concurrent access, scopes, resource cleanup, reflection or annotations. Only the configuration code sees the container; the coordinator still receives ordinary constructor arguments.
importjava.time.LocalDate;importjava.util.HashMap;importjava.util.HashSet;importjava.util.Map;importjava.util.Objects;importjava.util.Set;importjava.util.function.Supplier;publicfinalclassContainerWalkthrough{// Educational, single-threaded container: one lazy instance per registered type.// No reflection, annotations, scopes, resource cleanup or production guarantees.staticfinalclassDemoContainer{privatefinalMap<Class<?>,Supplier<?>>providers=newHashMap<>();privatefinalMap<Class<?>,Object>instances=newHashMap<>();privatefinalSet<Class<?>>constructing=newHashSet<>();public<T>voidbind(Class<T>type,Supplier<?extendsT>provider){Objects.requireNonNull(type,"type");Objects.requireNonNull(provider,"provider");if(providers.containsKey(type)){thrownewIllegalArgumentException("Duplicate binding: "+type.getSimpleName());}providers.put(type,provider);}public<T>Tget(Class<T>type){if(instances.containsKey(type))returntype.cast(instances.get(type));Supplier<?>provider=providers.get(type);if(provider==null)thrownewIllegalArgumentException("Missing binding: "+type.getSimpleName());if(!constructing.add(type))thrownewIllegalStateException("Dependency cycle: "+type.getSimpleName());try{Tinstance=type.cast(Objects.requireNonNull(provider.get(),"provider result"));instances.put(type,instance);returninstance;}finally{constructing.remove(type);}}}publicstaticvoidmain(String[]args){DemoContainercontainer=newDemoContainer();container.bind(ConfirmationFormatter.class,ConfirmationFormatter::new);container.bind(NotificationSender.class,RecordingNotificationSender::new);container.bind(ReservationCoordinator.class,()->newReservationCoordinator(Map.of(BorrowerCategory.STUDENT,newReservationRules(),BorrowerCategory.STAFF,newStaffReservationRules()),container.get(ConfirmationFormatter.class),container.get(NotificationSender.class)));ReservationCoordinatorfirst=container.get(ReservationCoordinator.class);ReservationCoordinatoragain=container.get(ReservationCoordinator.class);if(first!=again)thrownewAssertionError("Expected one coordinator instance.");System.out.println(first.reserve("B1","CAM1",LocalDate.of(2026,10,20),2));if(again.reservations().size()!=1)thrownewAssertionError("Expected shared state.");System.out.println("Repeated resolution shares the same coordinator and one reservation.");DemoContainermissing=newDemoContainer();try{missing.get(NotificationSender.class);thrownewAssertionError("Expected missing-binding failure.");}catch(IllegalArgumentExceptionexpected){System.out.println(expected.getMessage());}DemoContainercyclic=newDemoContainer();cyclic.bind(NotificationSender.class,()->cyclic.get(NotificationSender.class));try{cyclic.get(NotificationSender.class);thrownewAssertionError("Expected cycle failure.");}catch(IllegalStateExceptionexpected){System.out.println(expected.getMessage());}}}
6.2 Run and trace it
From inside after, first run the normal gate, then:
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
Registration stores a provider without running it. The first coordinator resolution runs its provider, resolves its formatter and sender, constructs the coordinator and caches it. The second resolution returns the same coordinator. Both references therefore observe the same reservation list.
A new DemoContainer with no sender registration produces the missing-binding example. A separate container whose sender provider resolves itself produces the cycle example. These expected failures are caught in the demonstration so it can print their meaning. A zero exit status means the walkthrough observed the intended cases, not that missing or cyclic configuration is valid.
The application gate clears generated class files. Recompile the optional example after another gate run. Compiling it into build/classes avoids a platform-specific multi-directory classpath for this small exercise.
6.3 Compare with a framework container
A framework container similarly needs configuration, resolution rules and lifetime decisions. Spring’s official introduction describes its role in creating, configuring and assembling application objects. A framework adds substantial facilities beyond this example; it does not remove the need to decide what is shared and which contracts the application uses.
No Spring dependencies, annotations or framework-run claims are included in this version. In an instructor-led framework lab, map each registration to the manual constructor graph first. Find the actual policy mapping and sender choice, then determine whether the coordinator is reused or recreated. If business methods look up services through the container, discuss that service-location choice explicitly.
7. Model design rationale
7.1 Client needs and contract boundaries
Client
Needed role
Deliberately excluded capability
Reservation coordinator
Notify an accepted reservation through configured output
Provider administration, output history inspection, identity management
Demonstration/check code
Inspect recorded notices
No need to add that operation to the coordinator’s role
Application entry point
Select implementations and supply configuration
No reservation acceptance decisions duplicated here
Console sender
Write to its supplied stream and format a notice
No borrowing-policy selection or reservation storage
NotificationSender serves an actual variation: console output and in-memory recording. Both implementations accept the same domain record under the same expected-failure convention. The recorder may expose additional operations on its concrete type without burdening the coordinator’s interface.
A proposed administration interface should only be created when an administration client exists. The exercise asks students to identify excluded operations, not to implement every hypothetical role they can imagine.
7.2 DIP, injection and composition
DIP: the coordinator depends on a small application role rather than console configuration or a provider SDK. Output implementations adapt to that role. The interface’s ownership and vocabulary matter, not merely its declaration.
Constructor injection: the coordinator receives the role implementation and other collaborators from its caller. This is ordinary Java object construction. Passing a concrete console sender through a concrete-typed parameter would still be injection, but would provide less provider independence.
Composition root:ReservationApplication selects the implementation and builds the object graph. It is allowed to reference concrete classes. Its factory does not fetch services on behalf of the coordinator during reservation processing; it supplies them before use.
Container: the optional walkthrough automates registered construction and reuse. It is kept outside the business object. The coordinator has no DemoContainer field or lookup and remains usable with manual assembly.
7.3 Assembly and runtime collaboration are different views
This object-assembly view shows the actual objects selected in the console demonstration. It differs from the source-dependency diagram in the student chapter.
Dotted arrows labelled “creates” start at the application entry point. Solid arrows into the coordinator mean collaborators supplied at construction. The sender also receives its own formatter and stream in the actual code; those details are omitted from this focused view to keep the central assembly relationship readable. The code supplies all of them explicitly.
A runtime trace has a different order: the coordinator evaluates eligibility and conflicts, stores a record, formats its result and invokes the already supplied sender. Do not infer event order from a dependency or construction diagram.
7.4 Why the constructor changed
Removing the no-argument constructor prevents the coordinator from hiding default provider choices. The price is source migration for callers. Existing setup expressions now use the application factory with a recorder, while the old reservation assertions remain unchanged.
An alternative compatibility constructor could delegate to a fixed default graph. That would preserve source compatibility but retain a default assembly decision inside or reachable from the coordinator. It may be justified in a published API with real callers; this teaching version has known source callers, so the migration is explicit and manageable.
Do not call the entire chapter a behaviour-preserving refactoring. Notification is new observable behaviour, expected notification failure has a new result, and constructor compatibility changes. The subsequent separation of construction preserves the agreed feature semantics under equivalent output configuration.
7.5 Failure policy and its limits
The chosen rule is: an accepted reservation remains accepted if the configured sender reports an expected output failure. The returned text distinguishes that situation. A failed sender does not convert acceptance into rejection, consume another identifier, trigger an automatic retry or create a background task.
This policy is small and observable. It is not reliable messaging. A future requirement for durable delivery might need persistent pending work, retry identity and an explicit result model. Those additions should follow an actual requirement and belong in later integration work, not be smuggled into a lesson on constructor injection.
Unexpected programming failures can still propagate after storage. The coordinator does not catch every runtime exception or erase evidence of bugs. The supplied formatter is deterministic and receives a valid immutable record, but the application is not a general transaction manager for arbitrary collaborators.
8. AI activity: model critique
The example is author-written fictional AI output. A useful response evaluates concrete dependencies and observable behaviour instead of judging the number of interface declarations.
Fixture claim or change
Verdict
Reason
Type the field as NotificationSender
Useful but insufficient alone
It expresses a role, but concrete construction can remain in the coordinator
Read provider settings inside the coordinator
Reject for this boundary
Environmental configuration still changes the reservation 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
Add configure/export/delete operations to the sender role
Reject without a client need
They couple this client to unrelated capabilities
Catch every runtime exception and return ordinary confirmation
Reject
It hides programming defects and makes expected output failure indistinguishable from ordinary success
Inject a global container for future lookups
Reject for this example
Real required collaborators become hidden behind a registry dependency
Declare DIP and DI complete because an interface exists
Reject
Contract ownership, provider knowledge and actual assembly must be inspected
A corrected answer supplies the three explicit constructor parameters, the application-owned sender contract, two implementations and external assembly. It preserves the stated notification timing and failure outcome. It also names the constructor migration and the still-limited in-memory model.
Independent verification includes inspecting concrete constructor calls, searching the coordinator for provider and container references, running with the recorder, and observing the failure case. A second model agreeing with the first is not a substitute for those observations.
9. Review-question solutions
9.1 Concept questions
1. Why omit received()? The coordinator’s job is to submit an accepted record, not inspect output history. The recorder’s concrete inspection API serves a different client, such as the check code. Adding it to the sender role would force a console implementation to invent a history it does not otherwise need.
2. Injection without the intended DIP boundary. A constructor taking ConsoleNotificationSender receives its collaborator from outside, so it uses DI. Its source still depends on that concrete provider. A contract copied directly from a provider SDK can also retain provider coupling despite having an interface name. Define the application role and inject a conforming implementation.
3. Knowledge in the root. The root knows which category maps to each concrete policy, which sender implementation is selected, which stream it uses and how formatter instances are constructed. The coordinator knows the collaborators’ roles and when to invoke them, not how their environment was chosen.
4. Sharing changes state lifetime. Two resolutions of one cached coordinator observe the same in-memory reservations. Two fresh coordinators have separate empty lists and can each accept what appears to be the first reservation. The method source can be identical while the assembled object graph changes the system’s behaviour. Neither caching nor fresh construction provides concurrency protection by itself.
9.2 Applied diagnosis
5. Delete after notification failure. The output may already have occurred, so deleting the reservation cannot undo it. The user could receive a notice for a record that no longer exists. Returning rejection also hides the earlier acceptance and may encourage another request. This chapter instead retains the record and reports the output failure explicitly; it does not imply the notice was never partially produced. Another policy requires an explicit consistency design, not a catch block pretending to reverse external effects.
6. What copying the map protects. The unmodifiable copy fixes associations between enum categories and policy object references. Changes to the original map cannot replace those references. The copy is shallow: an externally held policy object could still change internally if its implementation permitted it. The supplied stateless policy contract and implementations provide the additional stability. A collection wrapper cannot enforce arbitrary collaborator behaviour.
10. Practical activity: model reasoning and feedback
A satisfactory result identifies a genuine client role, supplies it from outside and explains failure semantics. It is not enough to replace a field type while leaving provider construction or settings lookup in a business method.
The work should keep three observations separate: existing reservation rules still behave as expected; the new notification feature has the stated effects; and collaborator assembly has moved to a visible boundary. Old tests alone address only the first. The 28 new checks and source inspection address selected risks in the other two.
For a peer walkthrough, ask the reviewer to substitute the recorder without editing the coordinator and explain where the notification goes. Next, ask them to identify the state after the controlled sender failure. Finally, ask which object owns the sender’s output stream. Retain actual findings; these prompts are not evidence that a review occurred.
Review concern
Evidence worth inspecting
Role is client-specific
Coordinator uses only the sender operation and application-domain record
Provider choice is external
Concrete output construction appears in assembly code, not the coordinator
Valid configuration
Null and missing-policy construction failures occur before request processing
Mapping is stable
Editing the caller’s original map does not rewire an existing coordinator
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 in the demonstration
Claims are bounded
No claim of email delivery, atomic messaging, thread safety or a hosted run
The optional resolver alternative is reasonable if resolving policy becomes a genuine client role (for example, if selection has its own stable contract beyond a fixed enum map. for the current two-entry mapping), another interface may add indirection without a new benefit. Either argument should be tied to the actual changes expected.
No numerical rubric is introduced here. Use the module leader’s approved criteria. The chapter’s completion conditions are learning checks, not new assessment weights or deadlines.
11. Transfer challenge: one possible solution
For the document converter, define a CompletionNotifier role that accepts an immutable successful-conversion result containing synthetic input/output names and the information needed for the notice. It should not validate conversion requests, perform conversion or administer user accounts.
The conversion workflow decides when conversion has succeeded. A desktop notifier can display the result, while an in-memory notifier records it. The application root selects one and supplies it through the converter’s constructor. The root also owns environment-specific objects such as the window or output destination.
A possible failure rule is: retain the successfully converted file and return a result distinguishing conversion success from notification failure. Do not delete the output simply because display failed, and do not claim background retry without implementing it. Whether that rule fits a real product must be agreed with its stakeholders.
Acceptance example
Conversion state
Notification state
Valid synthetic input converts successfully
One expected output produced
One completion result submitted
Conversion itself fails before output completion
No successful completion reported
No completion notice submitted
Conversion succeeds but notifier reports expected failure
Successful output retained
Failure reported separately; no automatic retry implied
A source dependency diagram can show the conversion workflow depending on its application-defined notifier role and both concrete outputs implementing it. Assembly code knows the desktop or recording implementation. No provider-specific window handle should leak into the conversion operation’s core contract.
The lesson transfers because it separates client need, provider detail, assembly and partial success. It does not require copying the equipment class names, using a map of policies, or choosing the same textual result representation.
12. Quiz 1 and teaching notes
Quiz 1’s stated coverage remains Weeks 1–4, corresponding to Chapters 2–5. Refer students to responsibilities and encapsulation, Agile artefacts and criteria, credible iteration planning and CI evidence, and SRP/OCP/LSP reasoning. Do not infer that holding the quiz in Week 5 adds ISP, DIP or containers to its assessed scope. Follow the module leader’s confirmed guidance.
For this week’s lab, begin with manual assembly. Ask learners to trace references before introducing the optional container. The container’s provider registrations should be recognisable as another representation of the same constructor graph. If a framework is used in class, identify its scopes and ownership rules rather than treating annotations as explanations.
Keep the hand-written recorder and failing sender vocabulary modest. They preview controlled collaborators, but Chapter 7 owns the full comparison of test-double roles and the introduction of a mocking framework. This chapter’s checks are supplied evidence, not a claim that a complete TDD cycle has been demonstrated.
The source remains a sequential in-memory version. Authoritative category lookup, loans and returns, cancellation and availability enquiry remain visible future work. Notification here is a local output port; later chapters can extend integration behaviour when there is a reason.
13. Fault experiment and container notes
13.1 Do the checks detect faults?
Fourteen faults were introduced one at a time into a copy of after, and the full gate was run on each. All fourteen are caught.
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
The student chapter’s required container lab (practical step 7) uses the educational container DemoContainer, listed in Section 6.1 of this file and runnable with plain javac. The module descriptor mentions a real library container; the book records that as a known difference and does not pin one. A team that wants to try a library container may do so on its own, with the dependency management that the plain-javac gate does not provide.
The supplied application gate stops on a nonzero check result. The container’s expected errors are caught inside its demonstration to verify those teaching cases; this is distinct from suppressing unexpected application test failures.
References
The companion uses the sources listed in the student chapter. Source checked date: 12 October 2026. The implementations, acceptance examples and model answers are original teaching material.