The interaction diagram is the method body, one message at a time.
π Where this lives: reading a well-written method should feel like reading the sequence diagram for it β same participants, same order, same shape. When it does not, something has been added that no design accounted for, and that is usually where the defects live: an extra call inserted under deadline pressure, a swallowed exception, a second responsibility grafted on. The correspondence between diagram and body is a genuine review tool. Search "single level of abstraction principle method".
The translation rules
EACH MESSAGE IN AN INTERACTION DIAGRAM BECOMES A STATEMENT IN
THE SENDER'S METHOD. The rules, systematically:
A MESSAGE TO ANOTHER OBJECT β A METHOD CALL ON THAT OBJECT
1: app := find(no) β Application app = repository.find(no);
A MESSAGE'S RETURN VALUE β A LOCAL VARIABLE
the name on the left of `:=` is the local variable's name
A Β«createΒ» MESSAGE β A CONSTRUCTOR CALL
2.1: Β«createΒ»(amt, m, o) β new Payment(amt, m, o)
A SELF-MESSAGE β A CALL TO A PRIVATE METHOD OF THE SAME CLASS
2.1: assertPayable(amt) β this.assertPayable(amt);
A GUARD β AN `if`
[status = APPROVED] 3: issue()
β if (status == Status.APPROVED) { issue(); }
AN ITERATION MARKER β A LOOP
*[for each line] 4: total := subtotal()
β for (ApplicationLine line : lines)
total = total.plus(line.subtotal());
A NESTED MESSAGE NUMBER β A CALL MADE INSIDE THE METHOD THAT
THE PARENT NUMBER NAMES
message 2 is `app.recordPayment(...)`, and 2.1, 2.2 are
calls made INSIDE recordPayment β so they belong in
Application's method, NOT in the handler's
THE MESSAGE ORDER β THE STATEMENT ORDER, and this is not merely
stylistic: the collaboration topic showed that allocating a
licence number before or after the status change changes
whether the minimal guarantee holds. THE ORDER IS PART OF THE
DESIGN.
A RETURN ARROW AT THE END β THE `return` STATEMENT
WHAT THE DIAGRAM DOES NOT GIVE YOU, and must be inferred:
Β· local variable TYPES (from the design class diagram)
Β· EXCEPTION HANDLING (from the use case extensions)
Β· TRANSACTION BOUNDARIES (from the supplementary spec)
Β· LOGGING (from the supplementary spec)
Β· null and empty-collection handling
Β· the exact form of loops and early returns
THESE ARE THE JUDGEMENTAL PARTS, and they are typically more
lines than the messages themselves.
Worked: two methods, from diagram to code
METHOD 1 β the handler, from the collaboration in ACtE0805.
THE DIAGRAM:
:IssueLicenceHandler
1: app := find(no) βββΆ :ApplicationRepository
2: licNo := allocate() βββΆ :LicenceNumberPool
3: lic := issue(licNo, officer)βββΆ :Application
4: save(app) βββΆ :ApplicationRepository
5: publish(event) βββΆ :EventPublisher
6: return lic
THE MECHANICAL TRANSLATION:
Licence handle(String no, OfficerId officer) {
Application app = repository.find(no); // 1
LicenceNo licNo = pool.allocate(); // 2
Licence lic = app.issue(licNo, officer); // 3
repository.save(app); // 4
events.publish(new LicenceIssued(lic)); // 5
return lic; // 6
}
SIX MESSAGES, SIX LINES. That is the mechanical part, and it is
genuinely mechanical. NOW ADD WHAT THE DIAGRAM COULD NOT SAY,
each addition traceable to a specific source:
@Transactional // supplementary spec:
// money and state must
// not diverge
public Licence handle(String no, OfficerId officer)
throws ApplicationNotFound,
NotIssuableException,
NumberPoolExhaustedException {
Application app = repository.find(no);
// find() throws ApplicationNotFound β declared, not
// caught: the caller (the UI) decides how to report
// a missing application. Use case ext. 2a.
LicenceNo licNo = pool.allocate();
// throws NumberPoolExhaustedException.
// ORDER MATTERS: allocation happens BEFORE any state
// change so that ext. 5a's minimal guarantee holds β
// a failed allocation leaves no half-issued
// application. See UC-05 ext. 5a.
Licence lic = app.issue(licNo, officer);
// throws NotIssuableException and
// SeparationOfDutiesException, both from the
// contract's preconditions. Not caught here.
repository.save(app);
// throws OptimisticLockException on the concurrent
// case, UC-05 ext. 5d. The transaction rolls back;
// the caller may retry.
events.publish(new LicenceIssued(lic));
// AFTER the save, so no event is published for a
// transaction that then fails. Printing, SMS and the
// dashboard subscribe β the invariant/reaction split.
log.info("licence {} issued for application {} by {}",
licNo, no, officer);
return lic;
}
COUNT WHAT THE NON-MECHANICAL PART ADDED: one annotation, three
declared exceptions, one ordering constraint with a reason, one
logging line, and five comments that each cite a requirement.
THE DIAGRAM GAVE SIX LINES; THE DESIGN CONTEXT GAVE THE REST,
and the rest is what makes it correct rather than merely
plausible.
METHOD 2 β the domain method, showing nested messages.
THE DIAGRAM, message 3 expanded:
3: issue(licNo, officer) :Application
3.1: assertTransition(ISSUED) [self]
3.2: isPaid() [self]
3.3: Β«createΒ» :Licence
3.4: changeStatus(ISSUED, officer) [self]
3.4.1: Β«createΒ» :StatusChange
THE CODE β note that 3.4.1 belongs inside changeStatus, not
inside issue, because the numbering says so:
public Licence issue(LicenceNo licenceNo, OfficerId officer)
throws NotIssuableException,
SeparationOfDutiesException {
assertTransition(Status.ISSUED); // 3.1
if (!isPaid()) // 3.2
throw new NotIssuableException("fee unpaid");
if (officer.equals(reviewedBy))
throw new SeparationOfDutiesException(
"issuer must differ from reviewer");
this.licence = new Licence(licenceNo, // 3.3
applicationNo,
clock.instant());
this.issuedBy = officer;
changeStatus(Status.ISSUED, officer); // 3.4
return licence;
}
private void changeStatus(Status to, OfficerId by) {
history.add(new StatusChange(status, to, by, // 3.4.1
clock.instant()));
this.status = to;
}
THE NESTING RULE IS WHAT KEEPS METHODS SHORT. If 3.4.1 were
inlined into issue(), the method would mix two levels of
abstraction β "record the issuance" and "append a history
row" β which is the SINGLE LEVEL OF ABSTRACTION principle
violated. The message numbering already told you where the
boundary is.
Method quality, and the smells
THE PROPERTIES OF A GOOD METHOD, and how each is checkable:
SHORT
A method that fits on a screen can be understood as a whole.
Long methods are almost always doing several things β which
the message-nesting rule would have prevented.
A ROUGH GUIDE, not a law: beyond about 20β30 lines, ask what
could be extracted.
ONE LEVEL OF ABSTRACTION
Every statement in a method should be at the same conceptual
level. A method that calls `validateApplication()` and then
manipulates a character array is mixing levels, and the
reader must switch mental gears mid-method.
ONE REASON TO CHANGE
From the cohesion ladder of ACtE0802: if two unrelated
requirements would both edit this method, it has two
responsibilities.
FEW PARAMETERS
Three or fewer is comfortable; beyond four, consider whether
the parameters form an object. `recordPayment(amount, method,
officer)` is fine; a method taking eight scalars is usually
missing a parameter object.
AND AVOID BOOLEAN FLAG PARAMETERS: `process(data, true)` is
unreadable at the call site and is control coupling from the
coupling ladder. Two methods beat one flag.
LOW CYCLOMATIC COMPLEXITY
From the design heuristics topic, V(G) = decisions + 1, and
it is a lower bound on the tests needed. A method with V(G) =
15 needs fifteen tests for basis-path coverage, which is a
concrete reason to simplify rather than an aesthetic one.
NO SURPRISES
A method named `getX` must not modify state. A query that
mutates is the defect nobody looks for, because the name
promised otherwise. THE COMMAND-QUERY SEPARATION principle:
a method either returns a value and changes nothing, or
changes something and returns nothing.
THE SMELLS, and the design defect each indicates:
SMELL THE UNDERLYING DESIGN DEFECT
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
long method several responsibilities;
message nesting ignored
long parameter list a missing parameter object, or
too many dependencies
a boolean flag parameter control coupling; should be two
methods
a chain of getters Law of Demeter violation;
(a.getB().getC()) the wrong object was asked
a type-testing conditional polymorphism was not used
(if instanceof / switch on
a type code)
duplicated code in two a missing abstraction
methods
a comment explaining WHAT the code is not clear; extract a
the code does well-named method instead
a method that needs six fan-out too high; the class has
doubles to test too many collaborators
feature envy β a method the behaviour belongs on the
that mostly uses another other class (Information Expert)
object's data
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
THE PATTERN ACROSS THE WHOLE TABLE: EVERY CODE SMELL IS A
DESIGN DEFECT MADE VISIBLE. That is why implementation feeds
back into design, and why the correct response to a smell is
usually to change the structure rather than to tidy the syntax.
REFACTORING is the disciplined response β from ACtE0802,
reorganising to simplify the design without changing behaviour.
The named moves that address the table above:
EXTRACT METHOD long method, comment-explaining-what
MOVE METHOD feature envy
INTRODUCE PARAMETER long parameter list
OBJECT
REPLACE CONDITIONAL type-testing conditional
WITH POLYMORPHISM
HIDE DELEGATE getter chain
REPLACE FLAG ARGUMENT boolean flag
WITH EXPLICIT METHODS
EACH IS SAFE ONLY UNDER TEST COVERAGE, which is the practical
reason tests precede refactoring rather than following it.
Command-query separation is the discipline most worth adopting from this topic: a method either returns a value and changes nothing, or changes something and returns nothing. A getX() that quietly mutates state is a defect nobody goes looking for, precisely because the name promised it was safe to call.
π Go further: the systematic catalogue behind the smell table is Fowler's Refactoring, which pairs each smell with named mechanical transformations β and its central insight is that these moves are behaviour-preserving, so they can be applied in small verified steps rather than as a risky rewrite. Modern IDEs implement many of them as automated commands (extract method, introduce parameter object, inline variable) with correctness guaranteed by the compiler rather than by care. The prerequisite is unchanged: refactoring without tests is just editing. Search "refactoring catalogue extract method behaviour preserving".
π‘ Exam angle: give the translation rules from an interaction diagram to a method body β a message becomes a call, a return value becomes a local variable, Β«createΒ» becomes a constructor call, a self-message becomes a private method call, a guard becomes an if, an iteration marker becomes a loop, and nested message numbers belong inside the parent's method. Be ready to write a method body from a given collaboration diagram. State what the diagram cannot supply β exception handling, transaction boundaries, logging, types β and where each comes from. Know the properties of a good method and be able to name code smells with the design defect each indicates and the refactoring that addresses it.
Syllabus points
Collaboration diagram β methods
Create a free account to tick topics off, take notes as you read, watch the video lessons and get a day-by-day study plan built around your exam date.
Related topics in Object-Oriented Design Implementation