Creating Class Definitions from Design Class Diagrams
Turning a box on a diagram into a compilable, correct class.
π Where this lives: code generators have been producing class skeletons from models since the 1990s, and the reason nobody ships generated code unedited is instructive: a generator emits the structure faithfully and gets everything interesting wrong β the constructor that should enforce an invariant, the field that should be immutable, the setter that should not exist. The skeleton is 20% of the work and 0% of the judgement. Search "code generation from UML why it fails".
The procedure
FROM ONE CLASS BOX to a complete class definition, in order.
STEP 1 β THE CLASS DECLARATION
the class name, and its modifiers from the diagram
italic name β abstract
Β«interfaceΒ» β interface
Β«enumerationΒ» β enum
a generalisation β extends
a realisation β implements
CONSIDER `final` for classes not designed for inheritance.
A class with no subclasses in the diagram and no protected
members is a candidate β inheritance is a design decision,
and permitting it accidentally is how fragile base classes
arise.
STEP 2 β THE FIELDS, from the attributes and navigable
associations
Β· type from the attribute's declared type, or from the
multiplicity for an association (previous topic)
Β· access modifier from the visibility marker β and in
practice fields are PRIVATE regardless, with visibility
expressed through methods
Β· `final` for every attribute marked {immutable}, and for
every collection field
Β· initialise collections at declaration, so they are never
null
STEP 3 β THE CONSTRUCTOR, and this is the step that carries the
design's meaning
Β· parameters: every field that must be set at creation β
which is every field whose multiplicity is 1 and every
{immutable} field
Β· VALIDATE EVERY INVARIANT the class is responsible for.
The constructor is the only place that can guarantee an
object is never in an invalid state.
Β· assign, then verify nothing else is required
A CLASS WHOSE CONSTRUCTOR TAKES NOTHING AND WHOSE FIELDS ARE
SET BY SETTERS CAN EXIST IN AN INVALID STATE, and every
method must then defend against it. Constructor validation
moves that check from N methods to one.
STEP 4 β THE METHODS, from the operations compartment
Β· signature exactly as declared, including exceptions
Β· bodies come from the interaction diagrams (next topic);
at this stage a declaration and a `TODO` is legitimate
Β· private helper methods that appear in the diagram
(assertTransition) are declared too
STEP 5 β THE SUPPORTING MEMBERS the diagram does not show but
the language requires
Β· `equals` and `hashCode` β see below, this is a real design
decision
Β· `toString` for diagnostics
Β· `compareTo` if the design says {ordered} on a collection
of this type
Β· serialisation concerns if applicable
STEP 6 β CHECK IT AGAINST THE DIAGRAM
every attribute present? every method? every association
represented with the right collection type? every
{constraint} enforced somewhere?
Worked: the Application class in full
FROM the design class diagram of ACtE0805, written out
completely. Every line traces to a design decision.
public final class Application {
// --- fields, from attributes and associations -------
private final ApplicationNo applicationNo; // 1, {imm}
private final LocalDate appliedOn; // 1, {imm}
private final District district; // βββΆ 1
private final LicenceCategory category; // βββΆ 1
private Status status; // 1
private OfficerId reviewedBy; // 0..1
private OfficerId issuedBy; // 0..1
private Payment payment; // βββΆ 0..1
private Licence licence; // βββΆ 0..1
private final List<StatusChange> history // β 1..*,
= new ArrayList<>(); // {ordered}
private int version; // solution
private final Clock clock; // injected,
// for tests
// --- constructor: enforces every invariant ----------
public Application(ApplicationNo no, LocalDate appliedOn,
District district,
LicenceCategory category,
Clock clock) {
this.applicationNo = requireNonNull(no);
this.appliedOn = requireNonNull(appliedOn);
this.district = requireNonNull(district);
this.category = requireNonNull(category);
this.clock = requireNonNull(clock);
this.status = Status.RECEIVED;
this.history.add(new StatusChange(
null, Status.RECEIVED, null, clock.instant()));
}
// --- queries ----------------------------------------
public Status currentStatus() { return status; }
public Money feeDue() { // / derived
return district.rateOn(appliedOn)
.forCategory(category);
}
public boolean isPaid() { // / derived
return payment != null && payment.isSettled();
}
public boolean wasEverRejected() { // expose the
return history.stream() // QUESTION,
.anyMatch(c -> // not the data
c.to() == Status.REJECTED);
}
// --- commands ---------------------------------------
public Payment recordPayment(Money amount,
PaymentMethod method,
OfficerId officer)
throws NotPayableException {
if (status != Status.APPROVED)
throw new NotPayableException(
"status is " + status);
if (payment != null)
throw new NotPayableException("already paid");
if (!amount.equals(feeDue()))
throw new NotPayableException(
"expected " + feeDue() + " got " + amount);
this.payment = new Payment(amount, method, officer,
clock.instant());
return payment;
}
public Licence issue(LicenceNo licenceNo,
OfficerId officer)
throws NotIssuableException,
SeparationOfDutiesException {
assertTransition(Status.ISSUED);
if (!isPaid())
throw new NotIssuableException("fee unpaid");
if (officer.equals(reviewedBy))
throw new SeparationOfDutiesException(
"issuer must differ from reviewer");
this.licence = new Licence(licenceNo, applicationNo,
clock.instant());
this.issuedBy = officer;
changeStatus(Status.ISSUED, officer);
return licence;
}
// --- private helpers -------------------------------
private void assertTransition(Status to)
throws NotIssuableException {
if (!Status.legalTransitions()
.contains(new Transition(status, to)))
throw new NotIssuableException(
status + " β " + to + " is not legal");
}
private void changeStatus(Status to, OfficerId by) {
history.add(new StatusChange(status, to, by,
clock.instant()));
this.status = to;
}
}
NOW READ WHAT THE CODE ENFORCES that the diagram only asserted:
1. THE CONSTRUCTOR CANNOT PRODUCE AN INVALID OBJECT. Every
multiplicity-1 field is required and non-null-checked, and
the initial status and its history entry are set together β
so no Application ever exists without a history.
2. THERE IS NO SETTER FOR `status`. The only paths to a new
status are recordPayment and issue, each of which validates.
THE INVARIANT IS STRUCTURAL: from outside the class, an
illegal transition is not merely forbidden, it is
inexpressible.
3. `changeStatus` IS PRIVATE AND ALWAYS APPENDS TO HISTORY. It
is impossible to change the status without recording it β
which is the audit requirement made unbypassable rather than
merely required.
4. `clock` IS INJECTED. The design said receivedOn is set at
creation; taking the clock as a dependency is what makes
every time-dependent assertion testable, per the FIRST
properties from the test automation topic.
5. THE THREE GUARDS IN `recordPayment` ARE THE THREE
PRECONDITIONS from the operation contract, in order. The
contract was not decoration; it is the guard list.
6. `wasEverRejected` RETURNS A BOOLEAN, NOT THE LIST. The
collection is never exposed, so the composition
relationship's ownership is real.
WHAT IS STILL MISSING, honestly: equals/hashCode, toString,
and β critically β nothing here is thread-safe. A single
Application instance mutated by two threads would corrupt its
own history. The `version` field records the intent to handle
that at the persistence layer with optimistic locking; the
in-memory object relies on being confined to one thread per
transaction. THAT ASSUMPTION SHOULD BE DOCUMENTED, because it
is invisible.
equals, hashCode, and immutability
TWO DECISIONS the diagram does not show and the language forces.
IDENTITY β WHICH KIND OF EQUALITY DOES THIS CLASS HAVE?
Two answers, and choosing wrongly causes subtle defects.
ENTITY (identity equality): two objects are the same if they
have the same identifier, regardless of their other fields.
An Application with number A-001 is the same application
whether or not its status has changed.
@Override public boolean equals(Object o) {
return o instanceof Application a
&& applicationNo.equals(a.applicationNo);
}
@Override public int hashCode() {
return applicationNo.hashCode();
}
β compare on the IDENTIFIER ONLY, and the identifier must be
immutable, or the object's hashCode changes while it sits
in a HashSet and it becomes unfindable.
VALUE OBJECT (structural equality): two objects are the same
if all their fields are equal. NPR 500 is NPR 500.
Money, ApplicationNo, LicenceNo, PersonName
β compare ALL fields, and make the class IMMUTABLE so the
comparison stays valid.
THE RULE: ENTITIES compare by identifier; VALUE OBJECTS
compare by state. Getting this backwards produces the classic
bug where two loads of the same database row are unequal, or
where a Money in a Set cannot be found after arithmetic.
THE CONTRACT `equals` AND `hashCode` MUST SATISFY:
Β· reflexive, symmetric, transitive, consistent
Β· equal objects MUST have equal hashCodes (the reverse is
not required)
Β· the fields used must not change while the object is in a
hash-based collection
VIOLATING THE LAST POINT is the commonest cause of "the item
is in the set but contains() returns false".
IMMUTABILITY β prefer it, and know what it buys:
Β· thread-safe with no synchronisation, because there is no
state to race on
Β· safe to share, cache and use as a map key
Β· no defensive copying needed on the way out
Β· impossible to be in an invalid state after construction
HOW: make every field final, take everything in the
constructor, return new instances rather than mutating:
public Money plus(Money other) {
requireSameCurrency(other);
return new Money(amount.add(other.amount), currency);
}
Note `plus` RETURNS a new Money. A mutating `add` would let
one holder of a Money change another holder's value β the
aliasing defect that immutability eliminates entirely.
WHERE IMMUTABILITY DOES NOT FIT: entities with a lifecycle.
An Application must change status; that is its purpose. THE
PRACTICAL RULE: value objects immutable, always; entities
mutable but only through methods that enforce the invariants.
That combination is what the Application class above
implements.
The most consequential line in that whole class is the one that is not there. Because no setStatus exists and changeStatus is private, changing the status without recording an audit entry is not merely forbidden β it is inexpressible. That is the difference between a rule and a structure.
π Go further: modern languages have absorbed much of this boilerplate. Java records, Kotlin data classes, C# records and Scala case classes generate the constructor, equals, hashCode and toString for value objects in one line β with structural equality and immutability as the defaults rather than something you remember to add. The interesting consequence is that the language now makes the right choice cheap and the wrong one verbose, which is the best kind of language design: the correct decision is the path of least resistance. Search "Java records value objects immutability".
π‘ Exam angle: give the procedure for creating a class definition β declaration with its modifiers, fields from attributes and associations, constructor enforcing invariants, methods with exceptions, supporting members, then a check against the diagram. Be ready to write out a full class from a diagram, and stress that the constructor validates the invariants and that fields carrying business rules have no setters. Distinguish entity equality (by identifier) from value-object equality (by state), state the equals/hashCode contract, and give the benefits of immutability β thread safety, safe sharing, valid-by-construction.
Syllabus points
Class diagram β class definition
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