Patterns Studio
Chapters 3–5 taught you the patterns; this studio makes you use them the way the assessments do: read a scenario, commit to a pattern, defend the choice, build it in C#, and articulate the difference against its look-alike. Six labs, in rising difficulty — four mirror the real Assignment 2 scenarios, one is a past-paper-shaped fusion exercise, and the capstone is a full point-of-sale engine.
How this studio works
Every lab is a scenario broken into three kinds of steps:
- Decide — commit an answer in the text box before opening any hints. Your text is saved locally in your browser, so be honest: this is your exam rehearsal.
- Build — open the companion project in
Labs-Code/labs/and make the failing xUnit tests pass. The tests are the spec. - Articulate — write the exam-style justification ("explain why X over Y for this scenario"), then compare against the model answer and its marking rubric.
You need the .NET SDK 10+ (dotnet --version). Each lab folder is independent:
cd Labs-Code/labs/<lab>/Starter && dotnet test shows the red tests, you implement the
// TODO markers until green. Every Starter/ compiles out of the box — failing tests are
the to-do list, not compile errors. Reference implementations live in Solution/; peek only after a
real attempt.
Labs 1–3 are the actual scenarios of Assignment 2 (2025) — the computer configurator (Q3–Q4), the legacy financial system (Q5), and the product catalogs (Q6) — extended with build stages the paper could not ask for. Lab 4 drills the behavioural patterns the paper loves to pair. Lab 5 is a fresh fusion scenario in the exact shape of past-paper Question 2. Lab 6 is a capstone that forces the patterns to coexist in one engine.
Lab 1 — The computer configurator creational structural
A computer manufacturing company offers highly customisable computers: CPUs, RAM configurations, storage
(SSD/HDD), GPUs, operating systems, and peripherals like monitors, keyboards and mice. A single constructor —
new Computer(cpu, ram, storage, gpu, os, monitor, keyboard, mouse) — would be unmanageable: many
parameters are optional, the order is confusing, building a "Gaming PC" vs an "Office PC" needs conditional
logic, and adding one optional component forces edits to the constructor and every caller.
Propose the application of a suitable creational pattern to construct customised computer
objects (Office, Gaming, Workstation). Then consider how the already built computers are represented,
enhanced, or organised afterwards.
Name the pattern. Then eliminate at least two plausible-sounding alternatives — the assignment's phrase "different types of computers" is bait for one of them. (Refresher: choosing a creational pattern.)
Hint 1 — nudge
Count the products in this scenario. Is the problem "which class do I instantiate?" or "how do I assemble one class that has too many optional parts"?
Hint 2 — stronger
An Office PC and a Gaming PC are not different classes — they are different configurations
of the same Computer. Which pattern separates the construction steps from the finished
representation, and which companion object stores named recipes?
Model answer & rubric
Builder — the scenario is a textbook
telescoping-constructor problem: one complex product (Computer) with many optional parts
and confusing parameter order. Builder replaces the giant constructor with step-by-step calls
(SetCpu(...), SetRam(...)…), so a client calls only the steps it needs, in any order,
and finishes with Build(). A Director then captures the named presets — Office,
Gaming, Workstation — as reusable recipes, so client code never repeats a parts list and adding a new preset
touches nothing else.
Why not the others: Factory Method answers
"which concrete class?" — but here there is only one product class with many configurations, so
subclass-per-configuration (GamingComputer, OfficeComputer…) explodes the hierarchy
for no behavioural difference. Abstract Factory
creates families of related products that must match (e.g. matching GUI widgets); computer parts are
picked independently, not in enforced families. Prototype solves expensive copying; Singleton solves instance
count — neither is the complaint in the scenario.
- ✔ Full credit: names Builder and maps it to the symptoms (optional parameters, ordering, presets), and introduces the Director for Office/Gaming/Workstation, and rejects at least one alternative with a correct reason.
- ◐ Partial: names Builder with a generic definition but never touches the scenario's specifics, or omits the Director role for the presets.
- ✘ Common mistake: choosing Abstract Factory because "there are many computer types" — the types here are configurations of one product, not matched families of different products.
The assignment says: "Draw the class composition in UML format and describe the relationships of the classes." Sketch it on paper first — builder interface, concrete builder, director, product — then list your classes and relationships below.
Model UML & common drawing mistakes
Computer is put together. The client asks the Director and never sees a parts list.- ✘ Solid-line inheritance arrow from
DesktopComputerBuilderto the interface — realization is a dashed line with a hollow triangle, pointing at the interface. - ✘ Director drawn creating
Computerdirectly — the Director only talks toIComputerBuilder; the dependency onComputerbelongs to the concrete builder'sBuild(). - ✘ Missing the «interface» stereotype, or omitting
Build() : Computer— the return type is the whole point of the pattern.
Implement steps 1–2 of lab P01: the fluent setters and Build() of
DesktopComputerBuilder, then the three Director presets.
Red tests Step1_Builder_AssemblesOnlyTheStepsYouCall, Step1_Builder_SettersAreChainable
and the three Step2_Director_* presets are your spec. Green proves: setters chain (each returns the
builder), untouched steps keep defaults, Build() hands over the product and resets, and each preset
yields a correctly configured machine from one call.
Reference (peek after attempting)
public IComputerBuilder SetCpu(string cpu) { _computer.Cpu = cpu; return this; }
// ... every setter mutates, then returns this — that is what makes chaining work.
public Computer Build()
{
var result = _computer;
_computer = new Computer(); // reset so the builder is reusable
return result;
}
public Computer BuildGamingPc(IComputerBuilder b)
{
var c = b.SetCpu("Intel i7-14700K").SetRam(32).SetStorage("2TB NVMe SSD")
.SetGpu("RTX 4070 Ti").SetOs("Windows 11")
.AddPeripheral("144Hz Monitor").Build();
c.BasePrice = 1800m;
return c;
}
What to notice: the Director method reads like the marketing spec sheet — that's the win. The recipe lives in exactly one place, and the client never learns what a gaming PC contains.
Q4's twist: the computers are now built. Marketing wants (a) optional add-ons — RGB lighting, an extended warranty — attachable to any single machine at runtime, stacking price and description; and (b) bundles — "Office Package" = one machine + monitor + keyboard + mouse, and bundles can contain other bundles — priced as one thing. Identify the two structural patterns (hint from the paper: dynamic feature addition and hierarchical composition) and say which handles which.
Hint 1 — nudge
Requirement (a) wraps one object to give it more. Requirement (b) groups many objects to treat them as one. Two different chapter-4 patterns own those two shapes.
Hint 2 — stronger
Both patterns can implement the same component interface (Describe(),
Price()). One holds a single Inner reference and adds to its answers; the other holds
a List<IPricedComponent> and aggregates. See
Decorator and
Composite.
Model answer & rubric
(a) Decorator: RgbLighting and ExtendedWarranty wrap an
IPricedComponent, adding their surcharge and description to whatever they wrap. Features stack by
nesting wrappers around one machine at runtime — no GamingPcWithRgbAndWarranty subclass
explosion. (b) Composite: a ComputerBundle is itself an
IPricedComponent that contains children (machines, parts, other bundles); Price()
recurses down the tree, so the client prices a nested package with one call, exactly as it prices one part.
- ✔ Full credit: both patterns named, each mapped to the right requirement, with the structural reason (wraps one vs contains many) stated in the scenario's own terms.
- ◐ Partial: patterns named and assigned correctly, but justified only with textbook definitions — no computers, add-ons, or bundles mentioned.
- ✘ Common mistake: swapping them — using Composite for add-ons ("the machine is composed of features") or Decorator for bundles. The lifetime test: an add-on has no meaning alone; a bundle's children are whole products in their own right.
Implement steps 3–4 of P01 in Structural.cs: the two feature decorators, the
Part leaf, and the recursive ComputerBundle.
Green on Step3_Decorator_StacksFeatureCostAndDescription proves the wrappers stack
(600 + 45 + 120 = 765 on one machine). Green on the two Step4_Composite_* tests proves the bundle
recurses over nested bundles and that a decorated machine drops into a bundle unchanged — both patterns
speak IPricedComponent.
Reference (peek after attempting)
// Decorator: ONE inner component, answers modified.
public override decimal Price() => Inner.Price() + 45m;
// Composite: MANY children, answers aggregated.
public decimal Price() => _items.Sum(i => i.Price());
What to notice: the two implementations are one line each, and the whole exam answer is
visible in the difference between those lines — Inner. versus _items.Sum.
The assignment's exact demand: "Clearly articulate the main conceptual difference between these two chosen structural patterns in the context of the Computer scenario, highlighting why you would choose one over the other based on specific post-construction requirements (e.g., adding features vs. managing internal parts)." Write it as you would in the paper — then compare.
Model answer & rubric
Both patterns rely on recursive composition against a shared component interface, but their intent
differs. Decorator is about responsibilities: it wraps exactly one component and adds behaviour
or state to it (RGB lighting adds 45 to that machine's price and text to its description) —
the object count stays one; only its capabilities grow. Composite is about structure: it
represents a part–whole hierarchy in which a group (Office Package) and an individual (a mouse, one PC) are
treated uniformly — the point is aggregation over many children, not enhancement of one. Decision
rule for this scenario: a requirement phrased as "add a capability to an existing computer at runtime"
→ Decorator; phrased as "organise computers and parts into bundles priced as one" → Composite. They
also compose: a warranty-decorated machine sits inside a bundle because both are
IPricedComponent. (Deeper contrast: Composite
vs Decorator.)
- ✔ Full credit: contrasts intent (add responsibility vs represent hierarchy), notes the one-child vs many-children structure, and gives a scenario-anchored decision rule.
- ◐ Partial: correct definitions but the "why choose one over the other" is generic — no post-construction requirement cited.
- ✘ Common mistake: "Decorator adds features, Composite groups things" with no structural reason — that restates the hint and earns the minimum.
Lab 2 — Modernising a legacy financial system structural
A critical financial reporting system has two components. The Legacy Reporting Module
generates daily reports and strictly expects data through ILegacyDataSource —
processFinancialRecordsInXml(String xml, …). It cannot be modified (criticality, no
documentation). The New Transaction Service is the authoritative source of real-time
transactions via retrieveTransactionsJson(…) : List<Map<String,String>> — robust, but
very expensive per call and restricted to authorised systems. Problem 1: the legacy module must
consume the new service despite the XML/JSON interface mismatch. Problem 2: the expensive call must be lazy,
cached for repeat requests, and refused to unauthorised callers.
Identify the structural pattern that lets the unmodifiable legacy module consume the new service, and explain its role in resolving the interface mismatch.
Hint 1 — nudge
Neither side can change: one demands ILegacyDataSource, the other offers JSON maps. What kind of
class stands in the middle and translates?
Hint 2 — stronger
The middle class implements the interface the client expects and holds a reference to the incompatible service, converting formats inside. That is chapter 4's first wrapper.
Model answer & rubric
Adapter. A
TransactionServiceAdapter implements ILegacyDataSource (the target the
legacy module already depends on) and wraps the new service (the adaptee). When the legacy module asks
for XML, the adapter calls retrieveTransactionsJson(), converts each JSON map into a
<transaction …/> element, and returns the XML string. Both existing components stay
untouched — the incompatibility is absorbed entirely by one new class, which is the pattern's whole purpose:
make two interfaces that already exist, and cannot change, work together.
- ✔ Full credit: names Adapter; identifies target (
ILegacyDataSource) and adaptee (the new service); states the conversion direction JSON→XML; stresses that neither existing side is modified. - ◐ Partial: names Adapter with a correct definition but never maps target/adaptee to the scenario's classes.
- ✘ Common mistake: proposing Facade — a Facade simplifies a subsystem behind a new convenient interface; here the interface is dictated by the legacy client and the job is translation, not simplification.
Identify the structural pattern that manages access to the expensive service, and explain how it delivers all three requirements: call only when genuinely needed, serve repeats from cache, refuse unauthorised callers.
Hint 1 — nudge
This class implements the same interface as the real service — the client can't tell them apart — but it decides whether, when, and for whom the real call happens.
Hint 2 — stronger
Chapter 4 names three classic flavours of this pattern: virtual (lazy), caching, and protection. This scenario asks for all three at once. See Proxy.
Model answer & rubric
Proxy. A
TransactionServiceProxy implements the same ITransactionService interface as the real
service and stands in front of it. Lazy: the proxy holds the reference but does not touch the real
service until the first data request. Cache: the first result is stored; repeat calls within the
window return it without a network round-trip. Protection: the proxy checks the caller's authorisation
before forwarding and throws if it fails — the expensive call never even starts. Because the interface is
identical, clients need zero changes; access control is a substitution, not a rewrite.
- ✔ Full credit: names Proxy; explicitly maps lazy, caching and protection behaviours to the scenario's three demands; notes the same-interface substitution property.
- ◐ Partial: names Proxy and one or two of the three behaviours.
- ✘ Common mistake: proposing Decorator — structurally similar wrapper, but its intent is adding responsibilities; Proxy's intent is controlling access. The scenario asks for control (cost, security), not new features.
Lab P02 gives you the unmodifiable LegacyReportGenerator and a
NewTransactionService that counts its expensive calls. You implement
TransactionServiceAdapter.GetRecordsAsXml() and the full
TransactionServiceProxy.
Five red tests: Step1–Step2 prove the XML translation feeds the unchanged legacy
report ("Daily Report: 2 transactions, total 350.50"); Step3 proves laziness
(CallCount stays 0 until the first request), Step4 proves caching (three requests, one
real call), Step5 proves protection (unauthorised → UnauthorizedAccessException,
real service never touched).
Reference (peek after attempting)
// Adapter — pure translation, no policy:
public string GetRecordsAsXml()
{
var rows = _service.RetrieveTransactionsJson();
var body = string.Concat(rows.Select(r =>
$"<transaction id=\"{r["id"]}\" amount=\"{r["amount"]}\" currency=\"{r["currency"]}\"/>"));
return $"<transactions>{body}</transactions>";
}
// Proxy — pure policy, no translation:
public List<Dictionary<string, string>> RetrieveTransactionsJson()
{
if (!_authorized)
throw new UnauthorizedAccessException("Caller is not authorised...");
return _cache ??= _real.RetrieveTransactionsJson(); // lazy + cached in one line
}
What to notice: the adapter changes the shape of the call and the proxy changes the conditions of the call — and neither knows the other exists. In production you would chain them: adapter → proxy → real service.
The paper's closer: "Clearly articulate the fundamental difference between the chosen patterns in terms of their primary goal and the type of problem they fundamentally solve, using the specifics of this scenario to illustrate your point."
Model answer & rubric
Both are structural wrappers around the new service, but they solve orthogonal problems.
Adapter changes the interface: it exposes a different contract
(ILegacyDataSource) from the one it wraps, because its goal is compatibility — the legacy
module and the new service could not talk at all, and the adapter translates JSON maps into the XML the legacy
side demands. Proxy preserves the interface: it exposes the same contract
(ITransactionService) as the real service, because its goal is access control — the
conversation was already possible but too expensive and too open, so the proxy governs when the call happens
(lazy), whether it happens at all (cache), and who may make it (auth). One-line test for the exam: if the
client couldn't call it before, you need an Adapter; if the client could call it but shouldn't call it like
that, you need a Proxy. (See also the wrapper family
compared.)
- ✔ Full credit: interface-changed vs interface-preserved stated explicitly, goals (compatibility vs control) named, both illustrated with the XML/JSON and cost/auth specifics.
- ◐ Partial: correct goals but no interface observation — the structural signature is what distinguishes wrappers under exam pressure.
- ✘ Common mistake: "Adapter converts data, Proxy caches data" — those are examples, not the fundamental difference; a marker wants intent and interface relationship.
Lab 3 — Heterogeneous product catalogs behavioral
An e-commerce platform integrates three suppliers: an Electronics Warehouse storing inventory in an optimised array; a Boutique Fashion Store using a linked list for fast insertion/removal; a Custom Crafts Workshop using a tree of categories. The central Product Display Module must traverse all products from any supplier, filter them (e.g. under $50), and total inventory value — without being coupled to any supplier's internal structure. Coupling would mean every structure change or new supplier forces edits to the display module, violating the Open/Closed Principle.
The paper asks four things: (1) the most appropriate behavioural pattern for uniform traversal over heterogeneous collections; (2) its core purpose and how it decouples the display module from catalog internals; (3) a mapping of the pattern's key components onto the scenario's elements; (4) the benefits for extensibility. Commit at least (1) and (3) before revealing.
Hint 1 — nudge
The display module wants to say "next product, next product, next product…" and nothing else. Which pattern extracts traversal out of the collection and hands the client a standard cursor?
Hint 2 — stronger
In C# the pattern is baked into the language: IEnumerable<T> is the aggregate contract,
IEnumerator<T> is the cursor, and foreach is the client syntax. See
Iterator.
Model answer & rubric
Iterator. Core purpose: provide a way to
access the elements of an aggregate sequentially without exposing its underlying representation. Each
catalog keeps its optimal structure internally but publishes the same traversal contract, so the display module
iterates identically over all three. Role mapping: Aggregate → IEnumerable<Product>
(the contract every catalog implements); Concrete aggregates → ArrayCatalog,
LinkedListCatalog, TreeCatalog; Iterator → the
IEnumerator<Product> each GetEnumerator() returns (array index walk, node walk,
category-bucket flattening); Client → the Product Display Module, which only ever writes
foreach (var p in catalog). Benefits: the display module is closed for modification — supplier D
with a hash map or database cursor is one new GetEnumerator(), zero display-module edits; filtering
and totalling are written once against the abstraction.
- ✔ Full credit: names Iterator, states the "sequential access without exposing representation" intent, maps all four roles to scenario elements, and makes the OCP argument concrete (new supplier = new enumerator only).
- ◐ Partial: names Iterator and its intent but maps roles vaguely ("the catalogs are the collections").
- ✘ Common mistake: proposing Visitor (that's for adding operations across a structure, and it must know the element types) or Strategy ("different traversal algorithms" — the requirement is uniform access, not swappable algorithms). See Iterator vs Visitor.
Lab P03: implement GetEnumerator() for the array, linked-list and tree catalogs
(use yield return), then the display module's TotalInventoryValue,
UnderPrice and CountAll — written once, against
IEnumerable<Product> only.
Step0 is a design assertion (already green) that all three catalogs implement the same iterator
contract. Greens on Step1–Step3 prove each structure walks correctly (array order,
insertion order, all category buckets); Step4–Step5 prove the same display
code totals and filters across all three structures.
Reference (peek after attempting)
// LinkedListCatalog — the iterator hides the nodes:
public IEnumerator<Product> GetEnumerator()
{
for (var n = _head; n is not null; n = n.Next)
yield return n.Value;
}
// The client never says "array", "node" or "tree":
public decimal TotalInventoryValue(IEnumerable<Product> catalog) => catalog.Sum(p => p.Price);
What to notice: yield return writes the Iterator pattern for you — the compiler
generates the state-machine cursor class the GoF book makes you hand-roll.
Q6 part 4: "Articulate the primary benefits that implementing this pattern would bring to the e-commerce platform's overall architecture and future extensibility." Make OCP the spine of the answer.
Model answer & rubric
The display module depends only on the abstraction IEnumerable<Product> — it is
closed for modification: no traversal, filtering or totalling code mentions arrays, nodes or
trees, so no supplier change can force an edit there. The system is open for extension at
exactly one seam: a new supplier implements GetEnumerator() for its own structure and immediately
works with every existing display feature. Consequences: (1) change amplification drops — a supplier swapping
linked list for a database cursor is a one-class change; (2) testing is localised — display logic is tested once
against the abstraction; (3) suppliers keep their performance-optimal internal structures, because uniformity is
imposed at the traversal contract, not on the data layout. This is the same DIP-flavoured move as the
OCP chapter's core example: high-level policy depends on an
interface, details plug in beneath it.
- ✔ Full credit: names what is closed (display module) and what is open (new aggregates), demonstrates with the "new supplier" walkthrough, and adds at least one secondary benefit (localised testing, preserved internal optimisation).
- ◐ Partial: says "it follows OCP" with a definition but no walkthrough.
- ✘ Common mistake: claiming the pattern makes traversal faster — Iterator buys decoupling, not performance; each structure was already optimal internally.
Lab 4 — Behavioural scenarios behavioral
Assignment 2 stopped at Iterator, but the past paper leans hard on the other behavioural patterns — notification flows, fixed skeletons, undo, and the State-vs-Strategy trap. Four short scenarios, one lab project (P04), scope kept tight.
An online store lets customers tap "notify me" on an out-of-stock product. When the warehouse restocks it, every subscribed customer must be told the new quantity — and customers subscribe and unsubscribe constantly at runtime. The store must not know whether a subscriber is an email sender, a mobile push service, or a test double. Propose the pattern and identify the participating roles.
Hint 1 — nudge
One object changes; many objects must hear about it; the set of listeners changes at runtime. Which pattern defines that one-to-many dependency?
Model answer & rubric
Observer. Subject:
StockItem, which owns the subscriber list and calls OnRestock(product, qty) on each
registered observer after a restock. Observer interface: IStockObserver — the only thing
the subject knows about its listeners, which is exactly what keeps email senders, push services and test
doubles interchangeable. Concrete observers implement that interface and react their own way. Subscription is
dynamic: Subscribe/Unsubscribe at any time, no recompilation, no subject changes.
- ✔ Full credit: names Observer, maps subject/interface/concrete observers to the scenario, and states the decoupling mechanism (subject depends only on the interface).
- ◐ Partial: names Observer but the interaction description is one-directional prose without roles.
- ✘ Common mistake: proposing Mediator — that centralises many-to-many peer communication; here one subject broadcasts to many passive listeners. See Observer vs Mediator vs Pub-Sub.
Implement steps 1–2 of P04: StockItem.Subscribe (no duplicates),
Restock (update quantity, then notify every current subscriber), and Unsubscribe.
Green proves: all subscribers hear each restock with the new cumulative quantity, an unsubscribed observer stops receiving, and double-subscribing does not double-notify.
Reference (peek after attempting)
public void Restock(int amount)
{
Quantity += amount;
foreach (var o in _observers.ToList()) // snapshot: safe if an observer unsubscribes mid-notify
o.OnRestock(Name, Quantity);
}
What to notice: the snapshot (.ToList()) is the classic production defence —
observers often unsubscribe from inside their own callback.
Every data-import pipeline in a reporting tool runs the same four beats in the same order: open the source, extract raw records, transform them, close the source. CSV and JSON imports differ only in the extract and transform beats — and no future pipeline may ever reorder or skip the open/close bracketing. Propose the pattern and say where the invariant lives.
Hint 1 — nudge
The algorithm's skeleton is sacred; only individual steps vary. Which pattern puts the skeleton in a base-class method and lets subclasses fill the gaps?
Model answer & rubric
Template Method. The abstract
DataPipeline.Run() is the template: it hard-codes open → Extract() →
Transform() → close. Subclasses (CsvPipeline, JsonPipeline) override
only the two protected abstract steps. The invariant is enforced structurally: the steps are
protected, so no client can call them out of order, and Run() is the only public
entry — the base class owns the order, forever.
- ✔ Full credit: names Template Method, locates the skeleton in the base-class method, names the overridable steps, and explains the visibility trick that protects the order.
- ◐ Partial: names the pattern but can't say what stops a subclass or client reordering the steps.
- ✘ Common mistake: proposing Strategy — Strategy swaps the whole algorithm by composition; here the algorithm is fixed and only steps vary by inheritance. See Template Method vs Strategy.
A document editor needs undoable edits: every append the user makes must be reversible with Ctrl-Z, in last-in-first-out order, and undo on an empty history must be a harmless no-op. Later the same mechanism should support queuing edits and logging them. Propose the pattern and explain where the undo knowledge lives.
Hint 1 — nudge
To undo, queue, or log a request, the request must first become a thing you can store.
Which pattern reifies an action into an object with Execute() — and, for undo, an
Undo()?
Model answer & rubric
Command. Each edit is reified as an
AppendTextCommand object carrying everything needed to perform and reverse itself: the
receiver (the document) and its parameters (the text). A CommandHistory holds executed commands on
a stack; Ctrl-Z pops the most recent and calls its Undo(). The undo knowledge lives inside
each command — the invoker only knows "pop and call Undo()", never how a particular edit
reverses. Because commands are plain objects, the same mechanism queues and logs them for free.
- ✔ Full credit: names Command, states the reification insight (request → object), locates undo inside the command, and names the stack-based history.
- ◐ Partial: names Command and Execute/Undo but puts the reversal logic in the history/invoker.
- ✘ Common mistake: proposing Memento alone — snapshots restore state, but the scenario asks for undoable operations plus queuing/logging, which needs the operation object itself. (Real editors often combine both.)
Implement steps 3–5 of P04: the DataPipeline.Run() template with its two
concrete pipelines, then AppendTextCommand and the stack-based CommandHistory.
Green proves: the skeleton order is fixed (open, read-csv, to-rows, close), the steps stay
protected (a reflection test checks!), execute/undo round-trips the document text in LIFO order, and
undo on empty history returns false instead of throwing.
Reference (peek after attempting)
public IReadOnlyList<string> Run() // the template — subclasses never touch it
{
var log = new List<string> { "open" };
log.Add(Extract());
log.Add(Transform());
log.Add("close");
return log;
}
public bool Undo()
{
if (_history.Count == 0) return false;
_history.Pop().Undo(); // the command reverses itself
return true;
}
What to notice: Run() is not virtual — that's deliberate. The pattern's power
is what subclasses cannot do.
A media-player object behaves differently depending on its current mode: when Playing, the play button pauses; when Paused, it resumes; when Stopped, it starts from the beginning. Pressing buttons changes the mode, and the transitions are the player's own business — clients just press buttons. A teammate says: "behaviour that varies — that's Strategy." Argue which pattern this is and why, giving the decision rule you'd use in the exam.
Hint 1 — nudge
Ask two questions: who chooses the active variant — the client, or the object itself? And do the variants know about and cause transitions to each other?
Model answer & rubric
This is State, not Strategy — even though
both delegate behaviour to interchangeable composed objects, which is exactly why the exam loves this trap.
Three discriminators, all pointing the same way: (1) Who selects? In Strategy, the client
picks an algorithm and injects it (choose a sort, choose a route). Here nobody injects "Paused" — the player
transitions itself when buttons are pressed. (2) Mutual awareness: strategies are
independent and ignorant of each other; states know their successors — Playing's
PressPlay() installs Paused. (3) Time: a strategy is typically fixed for the interaction;
state changes continuously over the object's lifetime and behaviour must track it. Decision rule: if the
variants form a transition graph the object walks by itself, it's State; if they're a menu the client orders
from, it's Strategy. (Full contrast: Strategy vs
State.)
- ✔ Full credit: commits to State, gives at least two discriminators (selector, mutual awareness, time-variance) and a scenario-anchored decision rule.
- ◐ Partial: correct choice justified only by "modes change" without the who-selects analysis.
- ✘ Common mistake: "they're basically the same pattern" — structurally near-twins, but intent differs, and the exam marks intent.
Lab 5 — Fusion: the TicketHub platform creational structural behavioral
You are designing TicketHub, an event-ticketing platform for a national arena network. The system must (A) create and manage various ticket types (Standard, VIP, EarlyBird — with E-Tickets and season passes expected later), (B) model each venue's physical layout, (C) handle fan interactions such as waitlists and a multi-step checkout, and (D) come together in one integrated class diagram. Answer each part before its reveal — in the exam this is one 25-mark question.
This mirrors the real past paper's Question 2 (a library system) beat for beat: creational part, structural part, two behavioural parts, then an integrated UML diagram. Work this lab in one 40-minute sitting for the most honest rehearsal — and keep the actual past paper unseen for your mock in chapter 17.
Box-office client code (SalesDesk, OnlineStore) must obtain ticket objects without
knowing each ticket's exact class, and new ticket types (E-Ticket, SeasonPass) will be added in future without
modifying client code. Propose the most suitable creational pattern and justify it against OCP.
Hint 1 — nudge
One product hierarchy (Ticket), unknown concrete class at the call site, future types by
extension. That is the founding creational question — not "complex assembly", not "matched families".
Model answer & rubric
Factory Method. Define an abstract
creator (TicketCreator, or per-channel creators) whose factory method
CreateTicket() returns the abstract Ticket; concrete creators
(VipTicketCreator, EarlyBirdTicketCreator…) override it to instantiate their type.
Client code holds only TicketCreator/Ticket references, so adding
ETicket is one new product + one new creator — existing clients are untouched: creation obeys
OCP. Builder would over-engineer (tickets aren't many-optional-part assemblies) and Abstract Factory is only
warranted if tickets came in enforced matched families.
- ✔ Full credit: Factory Method named, creator/product mapped to tickets, OCP argument via "new type = new subclass, no client edits".
- ◐ Partial: names it but justifies with "it creates objects" — every creational pattern does.
- ✘ Common mistake: Abstract Factory reflex whenever more than one type appears; families must co-vary to justify it.
A venue is organised as zones (North Stand, Floor), which contain sections, which contain rows or individual seats — and zones may contain sub-zones. Pricing and availability queries must treat a whole zone and a single seat uniformly ("how many seats are free in the North Stand?" / "is seat A-14 free?"). Propose the most suitable structural pattern.
Model answer & rubric
Composite. Component:
IVenueComponent (e.g. FreeSeats(), Price()); Composite:
Zone/Section holding child components and recursing; Leaf: Seat
answering for itself. The tree-with-uniform-treatment phrasing ("a zone can contain other zones as well as
seats") is the Composite signature — the same signal as shelves/sections in the past paper's library.
- ✔ Full credit: Composite + all three roles mapped + the uniform-query benefit stated.
- ✘ Common mistake: Decorator (nothing is being enhanced) or a plain aggregation drawing with no shared component interface — uniform treatment is the point.
Two behavioural needs: (i) when a sold-out event releases returned tickets, every fan on its waitlist must be alerted immediately (fans join and leave waitlists constantly). (ii) every checkout follows the same fixed sequence — validate the cart, reserve the seats, take payment, issue the tickets — but VIP checkouts add lounge passes at the issue step and E-Ticket checkouts issue by email. Propose one pattern for each and identify the key classes.
Model answer & rubric
(i) Observer — subject:
Event (holds the waitlist, publishes OnTicketsReleased); observer interface:
IWaitlistObserver; concrete observers: fan notifier channels (app push, email). Dynamic
subscribe/unsubscribe is the tell. (ii)
Template Method — abstract
CheckoutProcess.Run() fixes validate → reserve → pay → issue; subclasses
(VipCheckout, ETicketCheckout) override only the varying steps (notably
IssueTickets()). Same pairing the past paper's library question rewards for
notify-on-availability and the fixed borrowing procedure.
- ✔ Full credit: both patterns, with roles/classes named per part and the trigger phrase identified ("notify fans" / "fixed sequence, varying steps").
- ◐ Partial: right patterns, roles fuzzy or swapped between parts.
- ✘ Common mistake: Strategy for (ii) — the checkout sequence is invariant; only steps differ, and the base class must own the order.
On paper, draw the complete class diagram: the Factory Method creators and ticket products, the Composite
venue tree, the Observer waitlist, and the Template Method checkout — with correct relationship notation
(inheritance, realization, composition, association) and the seams where the patterns touch (a checkout issues
Tickets; a ticket references a Seat; an Event takes place in a venue).
Spend at least ten minutes before revealing.
Model UML & common drawing mistakes
- ✘ Four disconnected pattern islands — the 5-mark integration question is really asking for the cross-pattern edges (checkout→ticket, ticket→seat, event→observers).
- ✘ Composition diamond on the wrong end — the ◆ sits at the owner (
Zone), pointing its line at the children. - ✘ Solid triangles for interface realization (must be dashed + hollow), or arrows from parent to child instead of child to parent.
- ✘ Forgetting multiplicities where they carry meaning (
Zone 1 ◆— * IVenueComponent,Event —▸ * IWaitlistObserver).
Exam voice: "Explain how the proposed patterns collaborate to form a cohesive, flexible and maintainable architecture" — two or three sentences per pattern pair, naming the seam.
Model answer & rubric
The Factory Method isolates which ticket class exists, so the Template Method checkout can issue
tickets through the abstract Ticket without a single if (type == ...) — new ticket
types slot into an unchanged checkout. The Composite venue lets a Ticket hold one
Seat leaf while pricing and availability roll up zone trees of any depth — checkout reserves a
seat with the same call whether the fan bought one seat or a box. The Observer keeps Event
ignorant of notification channels, so releasing returned tickets (a checkout cancellation, say) triggers
waitlist alerts without coupling sales code to email or push infrastructure. Each pattern owns one axis of
change — ticket variety, venue shape, notification fan-out, checkout steps — which is why the design stays
open to the SRS's stated futures (E-Tickets, new zones, new channels, VIP perks) with only additive changes.
(Compare the worked fusion in chapter 6.)
- ✔ Full credit: every pattern tied to the axis of change it isolates, at least two seams described, and the "additive change only" claim demonstrated with a future requirement.
- ◐ Partial: four correct mini-definitions with no collaboration story.
- ✘ Common mistake: re-explaining each pattern in isolation — the question said collaborate; integration is what the marks are for.
Lab 6 — Capstone: the RetailCore POS engine capstone
RetailCore is a backend point-of-sale engine for retailers from coffee shops to supermarkets. It must decouple business logic from specific hardware (scanners, receipt printers) and payment providers, with runtime flexibility. Four phases: Inventory (Standard / Perishable / Digital products, protected stock), Hardware (segregated scanner/printer interfaces, injected drivers, runtime swap), Payment (Cash with change, Credit Card with Luhn validation, Store Credit with fund checks — new methods addable without touching the processor), and Pricing (stackable discount rules composed, never inherited). Lab P05 encodes every requirement as a test.
NFR-1.1: stock is private; nothing outside may set stock = -5. NFR-1.2: the cart treats all items
as an abstract product and must not need if (item is DigitalProduct) to total or sell. A digital
gift card is always sellable and never reduces stock; expired milk is unsellable even when shelved. Which OO
mechanisms (not GoF patterns — this phase is fundamentals) satisfy these, and where does each rule live?
Hint 1 — nudge
Three chapter-1 words: one hides and validates the field, one defines the contract, one lets each product
answer IsSellable()/ProcessSale() its own way.
Model answer & rubric
Encapsulation: _stock is private with no public setter; all mutation flows
through Restock()/ReduceStock(), which throw InvalidStockException on
anything that would go negative — the invariant lives inside Product, uncircumventable.
Abstraction: the abstract Product declares IsSellable(today) and
ProcessSale(today) — the whole contract the cart needs. Polymorphism: each
subclass answers differently — PerishableProduct checks expiry, DigitalProduct
returns a fresh DL- code and leaves stock untouched. The cart calls the same two methods on
everything, so no type-checks: the variation is inside the types, not in the caller.
- ✔ Full credit: all three mechanisms named and each SRS rule located in the right place (validation in the base, variation in overrides, contract in the abstraction).
- ◐ Partial: mechanisms named but the "where does the rule live" placement is missing.
- ✘ Common mistake: reaching for a GoF pattern — the SRS's own constraints are pure OO fundamentals here; patterns come in phases 2–4.
Work tests Step1_*–Step3_*: stock protection (including a reflection test asserting
no public setter), the perishable expiry rule, and the digital product's unique code + untouched stock.
Reference (peek after attempting)
protected void ReduceStock(int amount)
{
if (amount <= 0) throw new InvalidStockException("Reduction must be positive.");
if (_stock - amount < 0) throw new InvalidStockException($"Stock for '{Name}' cannot go negative.");
_stock -= amount;
}
What to notice: ReduceStock is protected — even subclasses go
through the validated path, and external code has no path at all.
A teammate proposes one IPeripheral interface with Scan() and
PrintReceipt(), and lets CheckoutTerminal instantiate new EpsonPrinter()
internally. The SRS forbids both (NFR-2.1, NFR-2.2) and demands switching to a digital receipt without
restarting (CONST-2.1). Name the two principles being enforced and describe the compliant design.
Model answer & rubric
Interface Segregation kills IPeripheral: a scanner cannot print, so a fat
interface forces degenerate implementations — split into IBarcodeReader and
IReceiptService so each driver implements only what it can honour.
Dependency Injection (applying DIP) removes the
internal new: the terminal receives both drivers via its constructor and holds them as interface
fields. Runtime swap then falls out for free — SwapHardware(printer: new EmailReceipt(...))
reassigns a field; the terminal's logic, typed against the interfaces, never notices.
- ✔ Full credit: ISP argued via the degenerate-method smell, DI/DIP argued via the constructor seam, and the swap explained as field reassignment behind the abstraction.
- ◐ Partial: principles named without the design consequences.
- ✘ Common mistake: calling the split "SRP" — SRP is about reasons to change in one class; forcing implementers to carry unusable methods is ISP's territory.
Work tests Step4_*–Step5_*: the ISP design assertions (neither interface leaks the
other's method), delegation through injected drivers, and the no-restart printer swap.
Cash computes change; Credit Card must pass a Luhn checksum before charging; Store Credit throws on insufficient funds; and NFR-3.1 demands a future "CryptoPay" be added without modifying the processor. Which pattern, and how do OCP and LSP show up in it?
Hint 1 — nudge
Interchangeable algorithms behind one interface, chosen per transaction by the caller — the same shape as the RetailCore SRS names outright in its non-functional requirements.
Model answer & rubric
Strategy. Interface:
IPaymentStrategy.ProcessPayment(amount); concrete strategies: CashPayment,
CreditCardPayment, StoreCreditPayment; context: PaymentProcessor, whose
Checkout simply delegates. OCP: CryptoPay is a new class implementing the
interface — the processor's code is provably untouched (the lab's Step8_Ocp_* test drops in a fake
crypto strategy and it just works). LSP: every strategy must be substitutable behind the
interface — the terminal calls ProcessPayment without knowing which is active, so no strategy may
strengthen preconditions or surprise the caller beyond the declared contract. Note this is client-chosen
algorithm selection — Strategy, not State: the payment method doesn't transition itself
(the trap from Lab 4 again).
- ✔ Full credit: Strategy with all three roles mapped, OCP shown via the drop-in extension, LSP shown via substitutability of the strategies.
- ◐ Partial: Strategy + roles, principles asserted but not demonstrated.
- ✘ Common mistake: an inheritance design (
PaymentMethodbase class with the processor switching on type) — the past paper's Q3 explicitly punishes this with "favour object composition over class inheritance".
Work tests Step6_*–Step9_*, then the integration test Step10_*: cash
change, the Luhn algorithm (right-to-left, double every second digit, subtract 9 above 9, sum % 10 == 0),
store-credit fund checks, the drop-in crypto strategy, and the discount pipeline — Happy Hour then Bulk then
Loyalty turns 100 into 74, rules that don't apply change nothing, and the total clamps at zero. Green on
Step10 means scan → price → pay → receipt runs end-to-end through every seam you built.
Reference (peek after attempting)
public decimal CalculateFinalPrice(CartInfo cart)
{
var total = cart.Subtotal;
foreach (var rule in _rules) // composition: a LIST of rules,
total = rule.Apply(total, cart); // applied in order, stacking naturally
return Math.Max(total, 0m);
}
What to notice: discount stacking is just iteration order. The inheritance
alternative (HappyHourCart, LoyaltyHappyHourCart…) needs a subclass per
combination — combinatorial explosion is the exam keyword.
NFR-4.1 forbids HappyHourCart-style subclasses. Make the case quantitatively: with 3 discount
rules today and 2 more coming, compare the inheritance design against the composed IDiscountRule
list — class counts, new-rule cost, and runtime flexibility.
Model answer & rubric
Inheritance encodes each combination as a class: 5 stackable rules means up to 25 = 32
cart subclasses (order-sensitive stacking is worse still), and adding rule #6 doubles it — modification, not
extension. Composition holds a List<IDiscountRule>: 5 rules = 5 small classes; rule #6 is
one new class and zero edits elsewhere (OCP); any subset stacks by adding to the list. Runtime: the composed
design reconfigures per transaction — Happy Hour switches on at 5 PM by adding a rule object — while the
inheritance design fixed the cart's class at construction. Same verdict the past paper's Q3 demands for
payment methods: favour object composition over class inheritance whenever behaviours combine or
change at runtime.
- ✔ Full credit: the combinatorial count (2n vs n classes), the extension-vs-modification contrast, and the runtime reconfiguration point.
- ◐ Partial: "composition is more flexible" with one supporting reason.
- ✘ Common mistake: arguing composition is always superior — the exam answer is that it wins when behaviours must combine or vary at runtime; inheritance remains right for true is-a specialisation like the Product hierarchy in Phase 1.
Closing quiz
Scenario-first, like everything in this studio. No pattern names in the questions — that's the exam's whole game.
Shaky on any Decide step? The disambiguation drills in chapter 12 target exactly these look-alike pairs. Solid everywhere? Move on to the Critique & Architecture Studio, then sit the mock exams cold.