Level 3 — ProductionDesign Principles

Design Principles

45 minutes60 marksprintable — key stays hidden on paper

Level: 3 — Production (from-scratch derivations, code-from-memory, explain-out-loud) Time Limit: 45 minutes Total Marks: 60

Instructions: Write code from memory where asked. Where an "explain out loud" prompt appears, write your explanation as if walking a reviewer through it. Pseudocode or any mainstream OO language (Python/Java/TypeScript) is acceptable unless stated.


Question 1 — DRY, KISS & YAGNI in tension (10 marks)

A junior engineer submits this pricing function. Assume TAX = 0.2.

def price_with_tax(items):
    total = 0
    for i in items:
        total = total + i.price + (i.price * 0.2)
    grand = 0
    for i in items:
        grand = grand + i.price + (i.price * 0.2)
    return grand, total, grand - total, [i for i in items if i.price > 999999]

(a) Identify one DRY violation and one YAGNI violation in this code, quoting the offending line(s). (4) (b) Rewrite the function to satisfy DRY, KISS, and YAGNI. It should return only the taxed grand total. (4) (c) Explain out loud: why can over-applying DRY sometimes conflict with KISS? Give a concrete one-sentence example. (2)


Question 2 — Single Responsibility, from scratch (10 marks)

The class below handles user registration.

class UserService:
    def register(self, email, password):
        if "@" not in email: raise ValueError("bad email")
        hashed = self._sha(password)
        self.db.execute("INSERT INTO users ...", email, hashed)
        smtp.send(email, "Welcome!")
        log_file.write(f"registered {email}")

(a) State the Single Responsibility Principle in one sentence, then list each distinct responsibility currently entangled in register. (4) (b) Refactor into SRP-compliant collaborators (class/interface names + method signatures; bodies may be stubs). Show how UserService.register now reads. (6)


Question 3 — Liskov Substitution derivation (10 marks)

(a) State the LSP in terms of substitutability. (2) (b) The classic Rectangle/Square problem: write Rectangle with set_width, set_height, area, then a Square that inherits from it. Demonstrate, with a short client function containing an assertion, exactly how Square breaks LSP. (5) (c) Explain out loud one design that avoids the violation, and state which precondition/postcondition rule was broken. (3)


Question 4 — Open/Closed via Strategy, code-from-memory (12 marks)

You must compute shipping cost by method (Standard, Express, Overnight), and new methods will be added often.

(a) Write a version using an if/elif chain and explain why it violates the Open/Closed Principle. (3) (b) Refactor using the Strategy pattern from memory: strategy interface, three concrete strategies, and a context class that uses one. (6) (c) Explain out loud how your design is now "closed for modification but open for extension" when a fourth method arrives. (3)


Question 5 — Dependency Inversion + Factory (10 marks)

A ReportGenerator currently does self.store = MySQLDatabase() in its constructor.

(a) State the Dependency Inversion Principle (both clauses). (3) (b) Rewrite ReportGenerator to depend on an abstraction, using constructor injection. Define the abstraction. (4) (c) Explain out loud how a Factory Method could supply the concrete implementation without ReportGenerator knowing the class name. (3)


Question 6 — Pattern identification & Interface Segregation (8 marks)

(a) For each scenario, name the single most appropriate GoF pattern: (4) i. Wrap an incompatible third-party logging API so it matches your Logger interface. ii. Notify many UI widgets automatically when a data model changes. iii. Ensure exactly one configuration object exists application-wide. iv. Build a complex immutable HTTP request step-by-step with optional parts.

(b) A Machine interface declares print(), scan(), fax(). A simple printer must implement all three but throws on scan/fax. Name the violated principle and show the segregated interface design. (4)

Answer keyMark scheme & solutions

Question 1 (10)

(a) (4)

  • DRY violation (2): the two loops computing total and grand are byte-for-byte identical (total = total + i.price + (i.price * 0.2) duplicated as grand = ...). Same logic written twice. (1 for identifying, 1 for quoting the duplicated lines.)
  • YAGNI violation (2): the returned grand - total (always 0) and the [i for i in items if i.price > 999999] filter are speculative outputs nobody asked for. (1 identify, 1 quote.) Also acceptable as KISS/magic-number note: literal 0.2 instead of TAX.

(b) (4)

TAX = 0.2
 
def price_with_tax(items):
    return sum(i.price * (1 + TAX) for i in items)

Marks: single computation, no duplication (1); returns only what's needed — YAGNI (1); uses named TAX, not magic number (1); simple/readable — KISS (1).

(c) (2) Forcing every superficially-similar snippet behind one shared abstraction can create a tangled, over-parameterised helper that is harder to read than the small duplication it removed (1). Example: merging "validate email" and "validate phone" into one regex-driven validate(field, kind) mega-function makes both harder to follow (1). (Two pieces of knowledge that happen to look alike are not the same knowledge — coincidental duplication.)


Question 2 (10)

(a) (4) SRP: a class should have only one reason to change / one responsibility (1). Entangled responsibilities (1 each, max 3):

  • input validation (email check)
  • password hashing (_sha)
  • persistence (DB insert)
  • email notification (smtp)
  • logging

(b) (6)

class UserRepository:
    def save(self, user): ...
 
class PasswordHasher:
    def hash(self, pw) -> str: ...
 
class EmailValidator:
    def validate(self, email): ...     # raises on invalid
 
class Notifier:
    def welcome(self, email): ...
 
class UserService:
    def __init__(self, repo, hasher, validator, notifier, logger):
        self.repo, self.hasher = repo, hasher
        self.validator, self.notifier, self.logger = validator, notifier, logger
 
    def register(self, email, password):
        self.validator.validate(email)
        user = User(email, self.hasher.hash(password))
        self.repo.save(user)
        self.notifier.welcome(email)
        self.logger.info(f"registered {email}")

Marks: distinct collaborators for validation/hashing/persistence/notification (3); dependencies injected not newed inside (1); register now only orchestrates (1); logging delegated (1). (UserService's one reason to change: the registration workflow.)


Question 3 (10)

(a) (2) LSP: objects of a subtype must be substitutable for objects of the supertype without altering the correctness of the program — a client using a base reference must work with any derived instance.

(b) (5)

class Rectangle:
    def set_width(self, w):  self.w = w
    def set_height(self, h): self.h = h
    def area(self):          return self.w * self.h
 
class Square(Rectangle):
    def set_width(self, w):  self.w = self.h = w
    def set_height(self, h): self.w = self.h = h
 
def client(r: Rectangle):
    r.set_width(5)
    r.set_height(4)
    assert r.area() == 20     # holds for Rectangle, FAILS for Square (16)

Marks: correct Rectangle (1), Square overriding both setters (2), client with assertion (1), correct demonstration that Square yields 16 not 20 (1).

(c) (3) Don't inherit Square from Rectangle; make both implement a common Shape interface with area() (no mutable independent width/height), or make shapes immutable so set_* doesn't exist (2). Rule broken: Square strengthens the postcondition/weakens a promise — the base's implicit invariant "setting width leaves height unchanged" is violated (postcondition of set_width broken) (1).


Question 4 (12)

(a) (3)

def cost(method, weight):
    if method == "standard":  return weight * 1.0
    elif method == "express": return weight * 2.0
    elif method == "overnight": return weight * 3.5 + 10

Violates OCP because adding a method requires editing this existing function (reopening tested code), risking regressions (2 for reasoning). (Existing, working code should not need modification to extend behaviour.) (1)

(b) (6)

from abc import ABC, abstractmethod
 
class ShippingStrategy(ABC):
    @abstractmethod
    def cost(self, weight): ...
 
class Standard(ShippingStrategy):
    def cost(self, w): return w * 1.0
class Express(ShippingStrategy):
    def cost(self, w): return w * 2.0
class Overnight(ShippingStrategy):
    def cost(self, w): return w * 3.5 + 10
 
class ShippingContext:
    def __init__(self, strategy: ShippingStrategy):
        self.strategy = strategy
    def calculate(self, weight):
        return self.strategy.cost(weight)

Marks: interface with abstract method (2), three concrete strategies (2), context delegating to injected strategy (2).

(c) (3) A fourth method (class Drone(ShippingStrategy)) is a new class implementing the interface — no existing strategy or the context changes (open for extension) (2); nothing in tested code is edited (closed for modification) (1).


Question 5 (10)

(a) (3) DIP: (i) high-level modules should not depend on low-level modules; both should depend on abstractions (1.5). (ii) abstractions should not depend on details; details should depend on abstractions (1.5).

(b) (4)

from abc import ABC, abstractmethod
 
class DataStore(ABC):          # abstraction
    @abstractmethod
    def read(self): ...
 
class MySQLDatabase(DataStore):
    def read(self): ...
 
class ReportGenerator:
    def __init__(self, store: DataStore):   # injected abstraction
        self.store = store

Marks: abstraction defined (1), concrete impl depends on abstraction (1), constructor injection (1), no new/instantiation of concrete inside (1).

(c) (3) A StoreFactory.create() returns a DataStore (declared return type = the abstraction). ReportGenerator (or its caller) calls the factory and receives an object typed as DataStore, never referencing MySQLDatabase by name (2); swapping to PostgresDatabase means changing only the factory (1).


Question 6 (8)

(a) (4) — 1 each: i. Adapter ii. Observer iii. Singleton iv. Builder

(b) (4) Violated: Interface Segregation Principle — clients shouldn't be forced to depend on methods they don't use (1). Design (3):

class Printer(ABC):
    @abstractmethod
    def print(self, doc): ...
class Scanner(ABC):
    @abstractmethod
    def scan(self, doc): ...
class Fax(ABC):
    @abstractmethod
    def fax(self, doc): ...
 
class SimplePrinter(Printer):     # implements only what it supports
    def print(self, doc): ...
 
class MultiFunction(Printer, Scanner, Fax):
    ...

Marks: split into small role interfaces (2), simple printer implements only Printer — no throwing stubs (1).


[
  {"claim":"Q1 taxed grand total for prices 100 and 200 at TAX=0.2 equals 360",
   "code":"TAX=Rational(2,10); prices=[100,200]; total=sum(p*(1+TAX) for p in prices); result = (total==360)"},
  {"claim":"Q1 grand-total minus total (duplicate loops) is always 0",
   "code":"p=symbols('p'); g=p+p*Rational(2,10); t=p+p*Rational(2,10); result = simplify(g-t)==0"},
  {"claim":"Q3 client sets width 5 then height 4: Rectangle area 20 but Square area 16, breaking assertion",
   "code":"rect_area=5*4; square_side=4; square_area=square_side*square_side; result = (rect_area==20 and square_area==16 and rect_area!=square_area)"},
  {"claim":"Q4 Overnight strategy cost for weight 4 equals 24",
   "code":"w=4; cost=w*Rational(35,10)+10; result = (cost==24)"}
]