STUDIO 15 · DO THE WORK

Patterns Studio

Chapters 35 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:

💡 Setup

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.

🧠 Why this order

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

📋 Scenario (Assignment 2, Q3–Q4)

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.

Decide Which creational pattern — and why not the other four?

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.)

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Decide Draw the UML before you peek

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.

Your answer is saved locally in your browser.
Model UML & common drawing mistakes
ComputerDirector BuildOfficePc(b) BuildGamingPc(b) BuildWorkstationPc(b) «interface» IComputerBuilder SetCpu(cpu) : IComputerBuilder SetRam(gb) / SetGpu(gpu) ... AddPeripheral(p) Build() : Computer DesktopComputerBuilder assembles the parts step by step Computer Cpu, RamGb, Gpu Storage, Os Peripherals drives creates
The Director knows recipes, the interface knows steps, only the concrete builder knows how Computer is put together. The client asks the Director and never sees a parts list.
Common drawing mistakes
  • ✘ Solid-line inheritance arrow from DesktopComputerBuilder to the interface — realization is a dashed line with a hollow triangle, pointing at the interface.
  • ✘ Director drawn creating Computer directly — the Director only talks to IComputerBuilder; the dependency on Computer belongs to the concrete builder's Build().
  • ✘ Missing the «interface» stereotype, or omitting Build() : Computer — the return type is the whole point of the pattern.
Build Builder + Director in C#

Implement steps 1–2 of lab P01: the fluent setters and Build() of DesktopComputerBuilder, then the three Director presets.

cd Labs-Code/labs/P01-computer-builder/Starter && dotnet test

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.

Decide After construction: two structural patterns

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Build Decorator + Composite on the same interface

Implement steps 3–4 of P01 in Structural.cs: the two feature decorators, the Part leaf, and the recursive ComputerBundle.

cd Labs-Code/labs/P01-computer-builder/Starter && dotnet test

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.

Articulate Decorator vs Composite, in this scenario

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.

Your answer is saved locally in your browser.
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.)

Marking rubric
  • ✔ 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

📋 Scenario (Assignment 2, Q5)

A critical financial reporting system has two components. The Legacy Reporting Module generates daily reports and strictly expects data through ILegacyDataSourceprocessFinancialRecordsInXml(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.

Decide Problem 1 — which pattern bridges XML and JSON?

Identify the structural pattern that lets the unmodifiable legacy module consume the new service, and explain its role in resolving the interface mismatch.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Decide Problem 2 — lazy, cached, authorised access

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Build The adapter and the proxy in C#

Lab P02 gives you the unmodifiable LegacyReportGenerator and a NewTransactionService that counts its expensive calls. You implement TransactionServiceAdapter.GetRecordsAsXml() and the full TransactionServiceProxy.

cd Labs-Code/labs/P02-legacy-adapter-proxy/Starter && dotnet test

Five red tests: Step1Step2 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.

Articulate The fundamental difference between the two chosen patterns

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."

Your answer is saved locally in your browser.
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.)

Marking rubric
  • ✔ 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

📋 Scenario (Assignment 2, Q6)

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.

Decide Which behavioural pattern — and map its roles

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.

Your answer is saved locally in your browser.
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: AggregateIEnumerable<Product> (the contract every catalog implements); Concrete aggregatesArrayCatalog, 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.

Marking rubric
  • ✔ 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.
Build Three enumerators, one client

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.

cd Labs-Code/labs/P03-catalog-iterator/Starter && dotnet test

Step0 is a design assertion (already green) that all three catalogs implement the same iterator contract. Greens on Step1Step3 prove each structure walks correctly (array order, insertion order, all category buckets); Step4Step5 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.

Articulate The OCP argument, in writing

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.

Decide Scenario A — "notify me when it's back"

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Build Observer: subscribe, notify, unsubscribe

Implement steps 1–2 of P04: StockItem.Subscribe (no duplicates), Restock (update quantity, then notify every current subscriber), and Unsubscribe.

cd Labs-Code/labs/P04-behavioral-set/Starter && dotnet test

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.

Decide Scenario B — a workflow with a fixed skeleton

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Decide Scenario C — an operation you can take back

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.)
Build Template Method + Command

Implement steps 3–5 of P04: the DataPipeline.Run() template with its two concrete pipelines, then AppendTextCommand and the stack-based CommandHistory.

cd Labs-Code/labs/P04-behavioral-set/Starter && dotnet test

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.

Articulate The trap — State or Strategy?

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.

Your answer is saved locally in your browser.
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.)

Marking rubric
  • ✔ 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

📋 Scenario (past-paper Question 2 shape — fresh domain)

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.

⚠️ Exam shape

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.

Decide A — creating ticket types flexibly

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Decide B — the venue's physical layout

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Decide C — waitlist alerts and the checkout skeleton

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Decide D — the integrated UML diagram

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.

Your answer is saved locally in your browser.
Model UML & common drawing mistakes
CREATIONAL · Factory Method STRUCTURAL · Composite BEHAVIOURAL · Observer BEHAVIOURAL · Template Method TicketCreator CreateTicket() : Ticket VipCreator EarlyBirdCreator «abstract» Ticket VipTicket EarlyBird creates «interface» IVenueComponent Zone Seat (leaf) ◆ children * Event Subscribe(o) ReleaseTickets() «interface» IWaitlistObserver AppPushNotifier / Email notifies * CheckoutProcess Run() // fixed skeleton # ReservePayIssue steps # IssueTickets() VipCheckout ETicketCheckout releases Ticket * Ticket ▸ Seat issues Ticket *
One diagram, four patterns: creators make tickets, tickets reference leaf seats in the composite venue, events notify waitlist observers when tickets release, and every checkout runs the fixed template that issues tickets. The highlighted edges are the seams the marker looks for.
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).
Articulate Why these four patterns cohere

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.

Your answer is saved locally in your browser.
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.)

Marking rubric
  • ✔ 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

📋 Scenario (from the OOP capstone SRS)

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.

Decide Phase 1 — products without type-checks

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?

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Build Phase 1 — inventory
cd Labs-Code/labs/P05-retailcore-capstone/Starter && dotnet test

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.

Decide Phase 2 — the hardware seam

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.
Build Phase 2 — hardware abstraction
cd Labs-Code/labs/P05-retailcore-capstone/Starter && dotnet test

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.

Decide Phase 3 — payments that extend, never modify

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?

Your answer is saved locally in your browser.
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).

Marking rubric
  • ✔ 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 (PaymentMethod base class with the processor switching on type) — the past paper's Q3 explicitly punishes this with "favour object composition over class inheritance".
Build Phases 3 & 4 — payment strategies and stacked discounts
cd Labs-Code/labs/P05-retailcore-capstone/Starter && dotnet test

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.

Articulate Composition over inheritance, with numbers

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.

Your answer is saved locally in your browser.
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.

Marking rubric
  • ✔ 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.

🎯 Where next

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.