Level 4 — ApplicationDesign Principles

Design Principles

60 minutes60 marksprintable — key stays hidden on paper

Level: 4 — Application (novel problems, no hints) Time limit: 60 minutes Total marks: 60

Answer all questions. Where code is required, any mainstream OO language (Python/Java/C#/TypeScript) is acceptable; be consistent. Justify every design decision by naming the principle(s) or pattern(s) applied.


Question 1 — Refactoring a Notification Monolith (14 marks)

A team ships this class (pseudocode):

class NotificationService:
    def send(user, message, kind):
        if kind == "email":
            smtp = connect_smtp("smtp.acme.com")
            body = "<html>" + message + "</html>"
            smtp.deliver(user.email, body)
            log_to_file("sent email to " + user.email)
        elif kind == "sms":
            gw = connect_twilio("KEY123")
            body = message[:160]
            gw.text(user.phone, body)
            log_to_file("sent sms to " + user.phone)
        elif kind == "push":
            fcm = connect_fcm("FCMKEY")
            fcm.notify(user.device_id, message)
            log_to_file("sent push to " + user.device_id)

(a) Identify three distinct design-principle violations present, naming the principle each time. (6) (b) Redesign the code (interfaces + a couple of concrete classes + the dispatch logic) so that adding a new channel (e.g. WhatsApp) requires no modification to existing classes. Name every principle/pattern you rely on. (6) (c) State one concrete symptom the client code would suffer before your redesign that disappears after it. (2)


Question 2 — Liskov & Interface Design (12 marks)

A payments library defines:

interface Account:
    withdraw(amount)
    deposit(amount)
    def overdraft_limit()  # returns a number

SavingsAccount implements withdraw by throwing NotAllowedError because savings accounts forbid withdrawals below zero, and returns overdraft_limit() = 0.

(a) Explain precisely whether SavingsAccount violates the Liskov Substitution Principle, and why. (4) (b) A PrepaidCard client only ever calls deposit. Explain which principle is violated by forcing PrepaidCard to depend on the full Account interface, and redesign the interfaces to fix it. (5) (c) Give one design guideline that would have prevented the LSP problem at the modelling stage. (3)


Question 3 — Pattern Selection Under Constraints (14 marks)

For each scenario, name the single most appropriate design pattern, classify it (creational / structural / behavioral), and give a one-line justification. Choosing a plausible-but-wrong pattern earns partial credit only if justified.

(a) A drawing app must let the user undo/redo an arbitrary sequence of edit operations. (3) (b) A legacy XmlReport class exposes getXmlString(), but your new reporting engine expects objects with render() -> HTML. You cannot change the legacy class. (3) (c) A game spawns thousands of identical tree sprites; each stores a 2 MB texture, but only its (x, y) position differs. (3) (d) A UI toolkit must build a family of matching widgets (DarkButton, DarkScrollbar) or (LightButton, LightScrollbar) so a whole theme is chosen at once. (3) (e) An order object must notify shipping, invoicing, and analytics subsystems whenever its status changes, without knowing who is listening. (2)


Question 4 — Dependency Inversion in a Novel System (12 marks)

You are designing a ReportScheduler that periodically generates reports and stores them. A junior writes:

class ReportScheduler:
    def run(self):
        db = MySQLDatabase("localhost")
        pdf = PdfGenerator()
        report = pdf.build(db.query("SELECT ..."))
        S3Uploader().upload(report)

(a) Explain why this violates the Dependency Inversion Principle, referencing "high-level" vs "low-level" modules. (4) (b) Rewrite ReportScheduler so it depends only on abstractions, showing the constructor/injection. Name where the concrete classes are now chosen. (5) (c) State one testing benefit that your inverted design unlocks, with a concrete example. (3)


Question 5 — Trade-offs & Critique (8 marks)

(a) A developer applies the Singleton pattern to a Configuration class and also to a DatabaseConnection class "to be safe." Give one legitimate reason and one serious risk of using Singleton here. (4) (b) "We should build a fully generic plugin architecture now, in case we ever support third-party extensions." The current product has one internal use case. Argue for or against, naming the two principles most in tension (one supporting, one opposing). (4)


Answer keyMark scheme & solutions

Question 1 (14 marks)

(a) Three violations (2 marks each):

  • DRY — the three branches repeat the same structure (connect → format body → deliver → log). The logging line is duplicated verbatim three times. (2)
  • Open/Closed Principle — adding a new channel forces editing the if/elif chain inside send; the class is not closed to modification. (2)
  • Single Responsibility Principle — the class knows SMTP details, Twilio details, FCM details, HTML formatting, truncation, and logging: many reasons to change. (Accept Separation of Concerns here too.) (2)

(b) Redesign (6 marks):

class Channel:                       # abstraction (DIP + OCP)
    def deliver(self, user, message): ...
 
class EmailChannel(Channel):
    def deliver(self, user, message):
        connect_smtp("smtp.acme.com").deliver(user.email, "<html>"+message+"</html>")
 
class SmsChannel(Channel):
    def deliver(self, user, message):
        connect_twilio("KEY123").text(user.phone, message[:160])
 
class NotificationService:
    def __init__(self, channels: dict[str, Channel], logger):
        self._channels = channels
        self._logger = logger        # logging extracted -> SoC / DRY
    def send(self, user, message, kind):
        self._channels[kind].deliver(user, message)
        self._logger.log(f"sent {kind}")

Marking: correct Channel abstraction (2); dispatch via polymorphism/registry instead of if-chain, so WhatsApp = new class only (2); naming Strategy/polymorphism + OCP + DIP and extracting logging (DRY/SoC) (2).

(c) Symptom (2 marks): Before — client (or maintainer) editing send risks breaking already-working email/sms code every time a channel is added (regression risk / recompile of a stable class). After — new channel is additive, existing tested code untouched. (Any equivalent concrete symptom.) (2)


Question 2 (12 marks)

(a) LSP (4 marks): Yes, it violates LSP. Client code written against Account may legitimately call withdraw; SavingsAccount strengthens a precondition / throws where the base contract implies success, so it is not substitutable for Account. A subtype must honour the base type's behavioural contract; throwing an unexpected exception breaks callers. (2 for verdict, 2 for contract-based reasoning.)

(b) ISP (5 marks): Forcing PrepaidCard to implement/depend on withdraw and overdraft_limit it never uses violates the Interface Segregation Principle — clients shouldn't depend on methods they don't use. Fix: split into role interfaces.

interface Depositable:  deposit(amount)
interface Withdrawable:  withdraw(amount); overdraft_limit()

class PrepaidCard implements Depositable
class CheckingAccount implements Depositable, Withdrawable
class SavingsAccount implements Depositable   # no bogus withdraw

Marking: name ISP (1); state the rule (1); segregated interfaces (2); note this also dissolves the LSP problem since SavingsAccount no longer promises withdraw (1).

(c) Guideline (3 marks): Model interfaces around capabilities/roles the client actually needs, not around real-world "is-a" taxonomy; or "prefer composition over inheritance"; or "design by contract — a subtype must accept everything the supertype accepts." Any one, well stated. (3)


Question 3 (14 marks)

Part Pattern Category Justification
(a) Command (+ Memento acceptable) Behavioral Encapsulates each edit as an object with execute/undo, enabling an undo/redo stack. (3)
(b) Adapter Structural Wraps XmlReport to expose the render()->HTML interface the engine expects without modifying legacy code. (3)
(c) Flyweight Structural Shares the intrinsic 2 MB texture across instances; extrinsic (x,y) passed in — massive memory saving. (3)
(d) Abstract Factory Creational Produces families of related products (button+scrollbar) guaranteed to match one theme. (3)
(e) Observer Behavioral Subject (order) notifies registered subscribers on state change without knowing them. (2)

Each: pattern name (1), correct category (1), justification (1); part (e) name+category = 2.


Question 4 (12 marks)

(a) DIP violation (4 marks): ReportScheduler is the high-level policy (when/what to report) but it directly instantiates low-level details (MySQLDatabase, PdfGenerator, S3Uploader). DIP says high-level modules should depend on abstractions, not concretions, and details should depend on abstractions. Here the high-level module is tightly coupled to specific vendors. (2 verdict, 2 high/low reasoning.)

(b) Rewrite (5 marks):

class ReportScheduler:
    def __init__(self, source: DataSource,
                 generator: ReportGenerator,
                 storage: Storage):
        self._source, self._gen, self._store = source, generator, storage
    def run(self):
        report = self._gen.build(self._source.fetch())
        self._store.save(report)

The concrete choices (MySQLDatabase, PdfGenerator, S3Uploader) are now selected in the composition root / main / DI container, not inside ReportScheduler. Marking: three abstractions as constructor params (3); run uses only abstractions (1); state that concretes move to composition root (1).

(c) Testing benefit (3 marks): You can inject a fake/in-memory Storage and a stub DataSource to unit-test run() with no real DB or network — e.g. FakeStorage that records calls, then assert save was called with the generated report. (Concept 2, concrete example 1.)


Question 5 (8 marks)

(a) Singleton (4 marks):

  • Legitimate reason: Configuration genuinely represents one global immutable state; a single shared instance avoids inconsistent config copies. (2)
  • Serious risk: Singleton is effectively global mutable state — it hides dependencies (violates DIP), makes unit testing hard (can't substitute a mock), and a shared DatabaseConnection singleton can become a concurrency bottleneck / break with connection pooling. (2)

(b) Speculative generality (4 marks):

  • Argument against building it now: YAGNI — only one internal use case exists; building a generic plugin system adds cost and complexity for unproven need. KISS also supports keeping it simple. (2)
  • Principle in tension supporting the build: Open/Closed Principle (extensibility without modification) — but OCP should be applied at known axes of change, not speculatively. Conclusion: keep it simple now, refactor toward extensibility when the second real use case appears. (2)

Accept a reasoned "for" answer if it explicitly names YAGNI as the opposing principle and justifies overriding it.

[
  {"claim": "Q1: number of distinct principle violations required equals 3",
   "code": "required=3; found=len(['DRY','OCP','SRP']); result = (found==required)"},
  {"claim": "Q3 has 5 pattern sub-parts covering all three GoF categories",
   "code": "cats={'a':'behavioral','b':'structural','c':'structural','d':'creational','e':'behavioral'}; result = (len(cats)==5 and set(cats.values())=={'creational','structural','behavioral'})"},
  {"claim": "Q4 inverted design injects exactly 3 abstractions",
   "code": "params=['source','generator','storage']; result = (len(params)==3)"},
  {"claim": "Total marks sum to 60",
   "code": "result = (14+12+14+12+8 == 60)"}
]