The language-level realities that no design diagram shows.
π Where this lives: the gap between a design diagram and working code is filled with language-specific decisions the diagram has no notation for β whether to use inheritance or composition, where a package boundary goes, how to construct an object with fifteen optional fields. Those decisions determine whether the codebase is pleasant in two years. And they differ by language: what is idiomatic in Java is wrong in Python, so "implementing the design" is never purely mechanical. Search "composition over inheritance why".
Inheritance versus composition
THE DESIGN SAID `Officer βββ CounterOfficer`. Whether that should
actually be inheritance in code is a separate question, and the
default answer is no.
WHAT INHERITANCE ACTUALLY COSTS:
Β· THE SUBCLASS DEPENDS ON THE SUPERCLASS'S IMPLEMENTATION,
not merely its interface. A change to a protected method
can break every subclass β the FRAGILE BASE CLASS
problem. This is the strongest coupling any two classes
can have.
Β· IT IS FIXED AT COMPILE TIME. An object cannot change its
class, so behaviour that varies over an object's lifetime
cannot be modelled by subtyping.
Β· IT IS SINGLE (in most languages), so you get one axis of
variation. Two independent axes β payment method and
notification channel β cannot both be inheritance
hierarchies without a combinatorial explosion of classes.
Β· IT BREAKS ENCAPSULATION between the two classes:
`protected` is a promise to every future subclass.
WHEN INHERITANCE IS RIGHT: when the relationship is genuinely
IS-A and satisfies the LISKOV SUBSTITUTION PRINCIPLE β
anywhere the supertype is expected, the subtype must work
without the caller knowing.
THE CLASSIC VIOLATION:
class Rectangle { setWidth(w); setHeight(h); }
class Square extends Rectangle { β¦ }
A Square must keep width and height equal, so
`setWidth(5)` then `setHeight(3)` behaves differently for a
Square than the Rectangle contract promises. Code written
against Rectangle breaks when handed a Square. THE
MATHEMATICAL "IS A" IS NOT THE PROGRAMMING "IS A" β a
square is a rectangle in geometry and not a subtype of a
mutable Rectangle in code.
COMPOSITION β the default alternative:
β class PriorityApplication extends Application {
// overrides half of Application's behaviour
}
β class Application {
private final ProcessingPolicy policy; // composed
Duration targetTurnaround() {
return policy.turnaroundFor(category);
}
}
Now the policy can differ per instance, can change at run
time, and can be tested alone. That is the Strategy pattern,
and it is what "favour composition over inheritance" means
concretely.
THE PRACTICAL RULE:
USE AN INTERFACE for "can be used as" β a contract with no
shared implementation
USE COMPOSITION for "is configured with" or "varies by"
USE INHERITANCE only for genuine specialisation where the
subtype adds to rather than replaces the supertype's
behaviour, and where LSP holds
AND CONSIDER `final` on classes not designed for
inheritance β permitting it accidentally is how fragile
hierarchies form
Packages, construction and language idiom
PACKAGE STRUCTURE β the code-level realisation of the layering
from ACtE0805, and a decision the class diagram does not make.
TWO COMPETING SCHEMES:
PACKAGE BY LAYER
com.nec.controllers β every controller
com.nec.domain β every entity
com.nec.repositories β every repository
β mirrors the architecture diagram
β a single feature is spread across every package, so a
change touches four directories; and nothing can be made
package-private, because collaborators live elsewhere β
so everything is public and the layering is advisory
PACKAGE BY FEATURE
com.nec.licensing.issue β handler, policy, tests
com.nec.licensing.payment β handler, Payment, gateway
com.nec.reporting β β¦
β a change lives in one directory
β genuinely allows package-private members, so a class
can be internal to a feature and INVISIBLE to the rest
of the system β real enforcement rather than convention
β shared domain concepts need a home, and the layering is
less visible at a glance
β MOST CURRENT PRACTICE FAVOURS BY FEATURE, with layering
enforced inside each feature package and by architecture
tests. The decisive argument is the package-private one:
by-layer packaging makes it impossible to hide anything.
CONSTRUCTION β what to do when a constructor has too many
parameters:
THE PROBLEM
new Application(no, date, district, category, clock,
null, null, null, null, 0)
unreadable, and adding a field changes every call site.
OPTION 1 β SEVERAL CONSTRUCTORS (telescoping). Works for
two or three; becomes unreadable beyond that, and cannot
distinguish two optional parameters of the same type.
OPTION 2 β A STATIC FACTORY METHOD with a meaningful name:
Application.newSubmission(no, date, district, category)
Application.rehydrate(snapshot) // from storage
β the name says what kind of construction this is, which
a constructor cannot
β can return a subtype or a cached instance
OPTION 3 β A BUILDER, for genuinely many optional fields:
Application.builder()
.number(no).appliedOn(date)
.district(d).category(c)
.build(); // validates the invariants HERE
β readable, order-independent, and `build()` is the single
validation point
β more code; unnecessary below about four or five
parameters
OPTION 4 β A PARAMETER OBJECT, when the parameters form a
concept: `new Application(ApplicationDetails details)`.
Often the best answer, because the new type turns out to
be a domain concept nobody had named.
THE RULE THAT MATTERS WHICHEVER IS CHOSEN: THE INVARIANTS
MUST STILL BE VALIDATED IN EXACTLY ONE PLACE. A builder
whose `build()` does not validate has merely moved the
problem and lost the constructor's guarantee.
LANGUAGE IDIOM β the same design, different code:
CONCERN JAVA / C# PYTHON
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private field `private` `_name` by convention;
enforced not enforced
interface `interface` a Protocol, or duck
typing
value object a record a frozen dataclass
null safety Optional<T> `| None` with type
hints
enum `enum` `enum.Enum`
package-private default access no equivalent;
`__all__` and
convention
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
THE POINT IS NOT THE TABLE BUT ITS CONSEQUENCE: a design that
relies on compiler-enforced encapsulation must be implemented
differently in a language that has none. In Python the
discipline is carried by convention, review and tests rather
than by the compiler β so the same design needs MORE test
coverage of its invariants, not less. Choosing to ignore that
is how "we implemented the design" produces a system whose
invariants are unenforced.
Worked: three classes, three different shapes
THE SAME SYSTEM NEEDS THREE KINDS OF CLASS, and treating them
alike is a common error.
1. A VALUE OBJECT β immutable, equality by state, no identity
public record Money(BigDecimal amount, Currency currency) {
public Money {
requireNonNull(amount); requireNonNull(currency);
if (amount.scale() > 2)
throw new IllegalArgumentException(
"money has at most 2 decimal places");
}
public Money plus(Money other) {
if (!currency.equals(other.currency))
throw new CurrencyMismatchException();
return new Money(amount.add(other.amount), currency);
}
public boolean isNegative() {
return amount.signum() < 0;
}
}
Β· a record: equals/hashCode/toString generated, fields final
Β· validation in the compact constructor, so an invalid Money
cannot exist
Β· `plus` RETURNS a new instance β no aliasing defects
Β· NO identifier, because two equal amounts ARE the same
value
2. AN ENTITY β mutable through guarded methods, equality by id
public final class Application {
private final ApplicationNo applicationNo; // identity
private Status status; // lifecycle
// β¦ as written in the earlier topic
@Override public boolean equals(Object o) {
return o instanceof Application a
&& applicationNo.equals(a.applicationNo);
}
@Override public int hashCode() {
return applicationNo.hashCode();
}
}
Β· identity is the business identifier, and it is final β
otherwise the object escapes its own HashSet
Β· state changes only through methods that validate
Β· NOT a record: records are for values, and an entity's
whole purpose is to change while remaining the same thing
3. A SERVICE β stateless, no identity, no state to compare
public final class IssueLicenceHandler {
private final ApplicationRepository repository;
private final LicenceNumberPool pool;
// dependencies only; NO request state
public Licence handle(String no, OfficerId officer) { β¦ }
}
Β· dependencies injected in the constructor, all final
Β· NO per-request fields, so one instance serves every
request concurrently β the statelessness argument from the
visibility topic
Β· equals/hashCode not overridden: there is no meaningful
equality for a service, and needing one is a sign it is
holding state it should not
THE DIAGNOSTIC TABLE:
question value entity service
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
has an identifier? no YES no
mutable? no yes* no
equality state id none
may be shared freely? YES no YES
holds request state? n/a n/a NEVER
ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
* only through methods that enforce the invariants
MOST DESIGN-TO-CODE MISTAKES ARE A CLASS IMPLEMENTED AS THE
WRONG ONE OF THESE THREE: a mutable value object (aliasing
bugs), an entity compared by state (two loads of one row are
unequal), or a service holding request state (fails under
concurrency, and only under concurrency).
The Square/Rectangle example is worth keeping because it shows that the mathematical "is a" is not the programming "is a". A square genuinely is a rectangle in geometry, and genuinely is not a subtype of a mutable Rectangle in code β because subtyping is a claim about substitutable behaviour, not about set membership.
π Go further: the way teams stop layering from decaying into convention is architecture tests β executable rules in the test suite. Libraries like ArchUnit let you assert "no class in ..domain.. may depend on ..infrastructure.." or "only handlers may be annotated @Transactional", and a violating import fails the build with the offending class named. It converts the layering rule from something a reviewer might notice into something CI enforces on every commit, which is the same "make the standard tool-enforced" principle from the SQA topic applied to architecture. Search "ArchUnit layered architecture test".
π‘ Exam angle: compare inheritance and composition, giving inheritance's costs (dependency on implementation, fixed at compile time, single, breaks encapsulation) and stating the Liskov Substitution Principle with the Square/Rectangle violation β that example is very commonly asked. Give the rule for choosing between interface, composition and inheritance. Compare package by layer with package by feature, noting the package-private argument. Know the construction options β telescoping constructors, static factory method, builder, parameter object β and that invariants must still be validated in one place. Distinguish value object, entity and service by identity, mutability and equality.
Syllabus points
Implementing classes
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