Skip to content

Bank 13 — SOLID and design patterns

Eleven exercises on responsibilities, substitution and the handful of patterns that solve recurring problems — factory, strategy, observer, decorator. Module 15. Levels L1–L6.

Solutions are in Solutions and answers; nudges are in Hints. Attempt first.


Exercises

E139 — Name the five L1 concept

Task. Name the five SOLID principles and give each in one sentence. Then match each principle to the symptom it prevents, choosing from: a class that must be edited every time a new kind is added; a subclass that cannot be used where its base is expected; an interface with methods most implementers do not need; a class with many unrelated reasons to change; a high-level module that imports a low-level detail.


E140 — The pricing strategy L2 predict

#include <iostream>
#include <memory>

class Pricing {
public:
    virtual ~Pricing() = default;
    virtual double price(double base) const = 0;
};

class Regular : public Pricing {
public:
    double price(double base) const override { return base; }
};

class Member : public Pricing {
public:
    double price(double base) const override { return base * 0.9; }
};

class Clearance : public Pricing {
public:
    double price(double base) const override { return base * 0.5; }
};

int main() {
    double base = 100.0;
    std::unique_ptr<Pricing> policy = std::make_unique<Member>();
    std::cout << policy->price(base) << '\n';
    policy = std::make_unique<Clearance>();
    std::cout << policy->price(base) << '\n';
}

Task. Write the two output lines. Which principle does passing the policy object in (rather than branching inside one class) demonstrate, and what does the context class now not need to know?


E141 — Complete the factory L2 complete

#include <iostream>
#include <memory>
#include <string>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
};

class Circle : public Shape {
public:
    explicit Circle(double r) : r_(r) {}
    double area() const override { return 3.14159 * r_ * r_; }
private:
    double r_;
};

class Square : public Shape {
public:
    explicit Square(double s) : s_(s) {}
    double area() const override { return s_ * s_; }
private:
    double s_;
};

// TODO: std::unique_ptr<Shape> makeShape(const std::string& kind, double size)
//       returning a Circle for "circle", a Square for "square", and nullptr otherwise.

int main() {
    auto a = makeShape("circle", 2.0);
    auto b = makeShape("square", 3.0);
    std::cout << a->area() << '\n';
    std::cout << b->area() << '\n';
}

Task. Write makeShape so the program prints 12.5664 and 9. Why does the function return unique_ptr<Shape> rather than Shape by value? What does the factory hide from main?


E142 — Who hears first? L2 trace scenario

A stock-ticker notifies its subscribers when a price changes.

#include <iostream>
#include <string>
#include <vector>

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(double price) = 0;
};

class Ticker {
public:
    void attach(Observer* o) { observers_.push_back(o); }
    void setPrice(double price) {
        std::cout << "tick " << price << '\n';
        for (Observer* o : observers_) o->update(price);
    }
private:
    std::vector<Observer*> observers_;
};

class Dashboard : public Observer {
public:
    void update(double price) override {
        std::cout << "dashboard " << price << '\n';
    }
};

class Alert : public Observer {
public:
    void update(double price) override {
        if (price > 100.0) std::cout << "alert high\n";
    }
};

int main() {
    Ticker ticker;
    Dashboard d;
    Alert a;
    ticker.attach(&d);
    ticker.attach(&a);
    ticker.setPrice(120.0);
    ticker.setPrice(90.0);
}

Task. Write every output line in order. Which pattern is this, and what does the Ticker need to know about Dashboard and Alert? (Answer precisely — it should be minimal.)


E143 — Add a policy, edit nothing L3 modify scenario

The pricing module from E140 must gain a flash sale policy (30% off) without editing Member, Clearance or any code that uses them.

Task. Add the new policy class and use it in main. Then state what you had to change in the existing classes — if the answer is nothing, name the design property that makes that possible (which SOLID principle?).


E144 — The factory that returns a ghost L3 debug

#include <iostream>
#include <string>

class Report {
public:
    virtual ~Report() = default;
    virtual void render() const = 0;
};

class Summary : public Report {
public:
    void render() const override { std::cout << "summary\n"; }
};

Report* makeReport(const std::string& kind) {
    if (kind == "summary") {
        Summary s;                 // local object
        return &s;                 // returning its address
    }
    return nullptr;
}

int main() {
    Report* r = makeReport("summary");
    if (r) r->render();
}

Task. Explain why r->render() may print nothing, crash, or print garbage — be precise about the lifetime of s. Then fix the factory so the caller receives a valid object and owns it, using the course's smart-pointer convention. What type should makeReport return now?


E145 — Wrap it twice L2 predict

#include <iostream>
#include <memory>
#include <string>

class Drink {
public:
    virtual ~Drink() = default;
    virtual std::string description() const = 0;
    virtual double cost() const = 0;
};

class Coffee : public Drink {
public:
    std::string description() const override { return "coffee"; }
    double cost() const override { return 2.0; }
};

class WithMilk : public Drink {
public:
    explicit WithMilk(std::unique_ptr<Drink> inner) : inner_(std::move(inner)) {}
    std::string description() const override { return inner_->description() + " + milk"; }
    double cost() const override { return inner_->cost() + 0.5; }
private:
    std::unique_ptr<Drink> inner_;
};

int main() {
    std::unique_ptr<Drink> d = std::make_unique<Coffee>();
    d = std::make_unique<WithMilk>(std::move(d));
    d = std::make_unique<WithMilk>(std::move(d));
    std::cout << d->description() << '\n';
    std::cout << d->cost() << '\n';
}

Task. Write the two output lines. Which pattern is this, and what does wrapping twice buy compared with having a bool hasMilk flag?


E146 — Many reasons to change L4 analyze

A single OrderService class validates orders, calculates tax, writes to the database, formats the receipt, sends the confirmation email, and logs to a file.

Task. (a) Name the principle it violates and list, in one line each, the distinct reasons it has to change. (b) Propose the classes you would split it into and the dependency direction between them. (c) Name one responsibility you might legitimately keep inside OrderService and why. (d) Give one symptom a maintainer would notice that signals this violation in the wild.


E147 — The account that refuses L4 debug scenario

A base type promises that withdraw succeeds when the balance is sufficient. A derived type breaks the promise.

#include <iostream>
#include <stdexcept>
#include <string>

class Account {
public:
    virtual ~Account() = default;
    virtual void withdraw(double amount) {
        if (amount > balance_) throw std::runtime_error("insufficient");
        balance_ -= amount;
        std::cout << "withdrew " << amount << '\n';
    }
    virtual double balance() const { return balance_; }
protected:
    double balance_ = 1000.0;
};

class FixedDeposit : public Account {
public:
    void withdraw(double) override {
        throw std::runtime_error("cannot withdraw from a fixed deposit");
    }
};

void cashOut(Account& a, double amount) {
    a.withdraw(amount);                 // assumes the base contract
    std::cout << "balance " << a.balance() << '\n';
}

int main() {
    FixedDeposit fd;
    try {
        cashOut(fd, 100.0);             // base says this should work
    }
    catch (const std::exception& e) {
        std::cout << "failed: " << e.what() << '\n';
    }
}

Task. Name the SOLID principle violated here and explain it precisely in terms of the contract (FixedDeposit is an Account, so anything valid for Account must be valid for it). Then fix the design: what should FixedDeposit not be? Give the corrected relationship.


E148 — Design a notification hub L5 design pattern scenario

A system raises events ("order placed", "payment failed"). Different consumers want to react differently: one emails, one writes a log, one updates a dashboard. More consumers will be added. Some consumers care about only one event type; others want all. At least one consumer must be able to stop receiving events at run time.

Task. Design the hub using patterns. Give:

  1. the observer interface and what it promises;
  2. how a consumer registers and, crucially, how it unregisters safely;
  3. whether a consumer is identified by event type, or decides internally, and why;
  4. the ownership rule for observers (who owns them, and what happens if one is destroyed while registered);
  5. one pattern you deliberately combined with observer, and why.

How you will be judged. On the unregistration and ownership rules — those are where observer designs go wrong.


E149 — Conditional to strategy L6 refactor pattern scenario

A shipping cost function grows a conditional every quarter:

double shippingCost(const std::string& method, double weight) {
    if (method == "standard") return 5.0 + weight * 0.5;
    else if (method == "express") return 12.0 + weight * 1.5;
    else if (method == "overnight") return 25.0 + weight * 3.0;
    else if (method == "economy") return 3.0 + weight * 0.2;
    // ... and so on
    return -1.0;
}

Task. Write a refactoring plan that replaces the conditional with a strategy (and a factory/registry if it helps). It must:

  1. name the strategy interface and one class per method;
  2. say how a caller looks up the right strategy from a string without a growing if;
  3. state what happens for an unknown method, and why returning -1.0 was wrong;
  4. identify one part of the calculation that is genuinely shared, and where it lives;
  5. say how you would add a fifth method and confirm you touched no existing class.

Success. Adding a method is a new class, not a new branch.


Previous topic: The STL with OOP · Next topic: Refactoring and design · Hints: hints.md · Solutions: solutions.md