What happens to the code when the design changes β which it will.
π Where this lives: the interesting thing about a mature codebase is not what it does but how easily it absorbs the next change. Two systems that behave identically today can differ by an order of magnitude in what it costs to add a field, and that difference is entirely a matter of how the classes were structured. Every technique in this topic is about making the second kind rather than the first. Search "cost of change codebase evolvability".
Where change requests land in code
A design change arrives as a change request, is evaluated and
approved through the process of ACtE0803, and then has to be
implemented. THE QUESTION THIS TOPIC ANSWERS: given an approved
change, what happens to the class definitions?
THE FOUR SHAPES OF CLASS-LEVEL CHANGE, in ascending cost:
1. ADD AN ATTRIBUTE
Add a field, add it to the constructor if it is required,
and update anything that constructs the class.
COST: proportional to the number of construction sites.
THE TRAP: making the new field required breaks every
existing caller, so a new field is usually added as
OPTIONAL first, backfilled, then tightened β the
expand/contract pattern from the release management topic,
applied to a class rather than a schema.
2. ADD OR CHANGE A METHOD
Adding a method breaks nothing. CHANGING A SIGNATURE
breaks every caller, and changing the BEHAVIOUR of an
existing method breaks callers silently, which is worse.
THE DISCIPLINE: adding an overload or a new method and
deprecating the old one lets callers migrate at their own
pace β the versioning rule from the interface
specification topic, at method scope.
3. MOVE A RESPONSIBILITY BETWEEN CLASSES
The Information Expert reasoning of ACtE0805 was applied
and got the wrong answer, or the domain has changed. This
is the MOVE METHOD refactoring, and it is the commonest
genuine design correction.
COST: moderate, and almost entirely mechanical under test
coverage.
4. CHANGE A RELATIONSHIP OR A MULTIPLICITY
The most expensive, because it propagates. Changing
Applicant 1β* Application to 1..*β* touches the field type,
every construction site, every query, every screen, every
validation and the database schema.
THIS IS WHY MULTIPLICITY QUESTIONS ARE ASKED OF THE DOMAIN
EXPERT DURING ANALYSIS β the associations topic's six
questions exist to avoid exactly this.
WORKED β one change request, costed through the code.
CR: "an application may have more than one applicant, for
commercial vehicles."
This is change shape 4, and it is the multiplicity question Q1
from the associations topic β asked late instead of early.
WHAT MOVES:
Application
- applicant : Applicant // was 1
+ applicants : List<Applicant> // now 1..*
the constructor 1 site, plus a guard that the
list is non-empty
every read of `applicant` found by the compiler β this is
the value of a typed language:
the change becomes a compile
error list rather than a
testing exercise
primaryApplicant() a NEW method, because most
screens want one name; without
it every caller invents its own
rule for which applicant to
show
the database a new join table, plus a
migration moving existing rows
the UI an add/remove list instead of
one field
validation "at least one applicant" and
"no duplicate citizen ids"
tests every fixture that built an
Application
THE MITIGATION THAT WOULD HAVE HELPED, in hindsight: if callers
had gone through `application.primaryApplicant()` from the
start rather than reading a public field, the internal change
from one reference to a list would have been invisible to most
of them. ENCAPSULATION IS WHAT MAKES A MULTIPLICITY CHANGE
SURVIVABLE, and that is a concrete, monetary argument for the
no-getters-on-collections rule rather than an aesthetic one.
Refactoring safely
REFACTORING is changing the structure of code WITHOUT CHANGING
ITS BEHAVIOUR. The definition matters: if behaviour changes, it
is not a refactoring, it is a modification, and the two must not
be mixed in one commit β because if a test fails you will not
know which one caused it.
THE PRECONDITION: TESTS. Refactoring without test coverage is
editing and hoping. From the test automation topic, that is why
tests precede refactoring rather than following it.
THE DISCIPLINE, per step:
1. run the tests β they must be green BEFORE you start
2. make ONE structural change
3. run the tests again
4. commit
Small steps mean a failure localises to the last change. A
two-hour refactoring with no intermediate test run is a
gamble whose stake is the whole session's work.
THE REFACTORINGS THAT MATTER MOST, matched to the design
defects of the previous topics:
EXTRACT METHOD a long method doing several things
β pull a named block into its own method. The single most
used refactoring, and the one that makes the "single level
of abstraction" property achievable.
MOVE METHOD feature envy β a method that uses
another object's data more than its own
β move it to the class that owns the data. This is
Information Expert, applied as a correction.
EXTRACT CLASS a class with two unrelated groups of
fields and methods (high LCOM from the
metrics topic)
β split along the attribute clusters. LCOM literally
measures which fields are used together, so the metric
tells you where the seam is.
INTRODUCE PARAMETER a long parameter list
OBJECT
β and often the new object turns out to be a domain concept
nobody had named, which is a modelling discovery rather
than a tidying exercise.
REPLACE CONDITIONAL a switch or if-chain on a type code
WITH POLYMORPHISM
β the Strategy pattern arrived at by refactoring rather
than by up-front design, which is usually the honest
route: you do not know a variation is real until you have
two of them.
REPLACE MAGIC NUMBER an unexplained literal
WITH A CONSTANT
β `if (days > 90)` becomes `if (days > RENEWAL_WINDOW_DAYS)`,
and the name is the documentation.
ENCAPSULATE FIELD / a public field, or a getter that
COLLECTION returns the internal collection
β the fix for the leak in the visibility topic, and the
prerequisite for surviving the multiplicity change above.
RENAME a name that no longer describes what
the thing does
β the cheapest refactoring and the most neglected. A method
called `validate` that also saves is a lie the compiler
cannot catch.
WHEN NOT TO REFACTOR:
Β· when the code is about to be deleted
Β· when you do not have tests and cannot cheaply write them
β write the tests first, or leave it alone
Β· in the same commit as a behaviour change
Β· when the "improvement" is speculative generality: adding
an interface with one implementation for a variation
nobody has requested is the over-engineering the patterns
topic warned about
Keeping design and code in agreement
THE PRACTICAL PROBLEM: after six months of change requests, do
the diagrams still describe the system?
THE THREE HONEST STRATEGIES, and the dishonest one:
(a) UPDATE THE MODEL WITH THE CODE
Every change that alters structure updates the diagram in
the same commit.
β the model stays true
β costs discipline on every change, and hand-drawn diagrams
make it costly enough that it lapses
β viable when the diagrams are TEXT (PlantUML, Mermaid) in
the repository, because then they are reviewable in the
diff and the reviewer notices when they were not updated.
This is the diagrams-as-code answer from the modelling
topic.
(b) REGENERATE THE MODEL FROM THE CODE
Reverse-engineer the class diagram on demand from the
source.
β never stale, zero maintenance
β shows STRUCTURE ONLY. It cannot show intent, cannot show
what was rejected, and produces the unreadable
200-class diagram unless filtered.
β useful for orientation in an unfamiliar codebase, useless
as a design record.
(c) KEEP ONLY THE MODELS THAT ARE STILL EARNING THEIR KEEP, AND
DELETE THE REST
β honest, cheap, and the surviving diagrams are trusted
β requires someone to make the judgement
β in practice the best default: a handful of curated
high-level diagrams maintained deliberately, plus
generated detail on demand.
(d) THE DISHONEST OPTION: leave the diagrams untouched and hope
nobody reads them. This is the stale-model failure from the
modelling topic, and it is worse than having no diagram,
because a reader will trust it and act on a false belief.
WHAT SHOULD ALWAYS BE UPDATED, whatever strategy is chosen,
because none of it can be regenerated:
Β· the ARCHITECTURE DECISION RECORD β what was chosen, what
was rejected, and why. A change that reverses an earlier
decision must say so, or the next engineer will
reintroduce the old approach.
Β· the GLOSSARY. A renamed domain concept must be renamed in
the glossary, or the ubiquitous language fractures.
Β· the OPERATION CONTRACTS for changed operations β
preconditions and postconditions are the specification
that tests are written from.
Β· the STATE MACHINE, if the legal transitions changed. This
is the artefact most often left stale, and the one whose
staleness causes the worst bugs, because everyone assumes
the transition set they remember.
TECHNICAL DEBT β the honest name for the gap between the design
you have and the design you would choose now.
DELIBERATE debt: "we know this should be split; we are
shipping for the deadline and will split it next
iteration." Recorded, and acceptable.
INADVERTENT debt: "we now understand the domain better and
would model it differently." Unavoidable, and the reason
refactoring is continuous rather than occasional.
THE DANGEROUS KIND: unrecorded deliberate debt, which
becomes indistinguishable from a design decision within a
year, and gets defended by people who assume it was
intentional.
THE PRACTICE: record it where the code is β a tracked issue
referenced from a comment, not a bare `// TODO` that nobody
will ever search for.
The most dangerous form of technical debt is unrecorded deliberate debt. A shortcut taken knowingly under deadline becomes indistinguishable from a considered design decision within a year β and will then be defended by people who assume someone had a reason.
π Go further: the practice that makes strategy (a) actually work is the architecture decision record: a short numbered markdown file per decision, committed beside the code, stating the context, the decision, the alternatives rejected and the consequences. Its value is precisely what a regenerated diagram cannot supply β why, and what we chose not to do. When a later engineer proposes the rejected option, the ADR either stops them or is updated to supersede itself, and the reasoning chain stays intact across staff turnover. Search "architecture decision records ADR superseded".
π‘ Exam angle: describe the kinds of change a class definition undergoes and rank them by cost, noting that multiplicity changes propagate furthest. Define refactoring precisely β structure changed, behaviour unchanged β and state that it requires tests and must not be mixed with behaviour changes in one commit. Name the main refactorings (extract method, move method, extract class, introduce parameter object, replace conditional with polymorphism, encapsulate collection, rename) and the design defect each addresses. Discuss the strategies for keeping models current and why a stale model is worse than none, and distinguish deliberate from inadvertent technical debt.
Syllabus points
Refining class definitions
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