Skip to content

Bank 14 — Refactoring and design

Eleven exercises on changing structure without changing behaviour, on reading a design critically, and on assembling a whole application. Modules 15 and 16. Levels L1–L6.

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


Exercises

E150 — The definition L1 concept

Task. Define refactoring in one sentence, and name the property a refactoring promises to preserve. Then answer: why is "rewrite the module from scratch" usually not a refactoring, and what must exist before a safe refactoring can begin?


E151 — Capture the behaviour first L2 trace scenario

Before touching legacy code, a team records what it currently does.

#include <iostream>

int classify(int n) {
    if (n < 0)        return -1;
    else if (n == 0)  return 0;
    else if (n < 10)  return 1;
    else if (n < 100) return 2;
    else              return 3;
}

int main() {
    int inputs[] = { -5, 0, 7, 42, 100, 999 };
    for (int n : inputs) std::cout << n << ':' << classify(n) << ' ';
    std::cout << '\n';
}

Task. Write the exact output — this is a characterisation test: it records the behaviour you must preserve. Then explain, in one sentence, why you write this before refactoring classify.


E152 — Extract a function L2 complete refactor

#include <iostream>

int main() {
    double radius = 3.0;
    double circleArea = 3.14159 * radius * radius;
    std::cout << circleArea << '\n';

    double r2 = 5.0;
    // TODO: reuse the same calculation instead of repeating the formula
    double a2 = 3.14159 * r2 * r2;
    std::cout << a2 << '\n';
}

Task. Extract the repeated calculation into a function named circleArea, call it twice, and keep the output identical (28.2743 then 78.5397 — six significant digits, the stream's default). What are the two benefits you can now point to, and what would happen if the value of pi changed?


E153 — Move the behaviour L3 modify scenario

A report printer has a 40-line main that formats a header, formats each row, and formats a footer, all inline.

Task. (a) Extract three functions with clear names. (b) Then decide whether one of them should instead become a class, and justify the choice in two sentences. (c) Give one signal that tells you "this should be a class, not a function". (No code listing of the 40 lines is required; describe the transformation.)


E154 — The refactor changed something L3 debug

A developer extracted the largest-value search out of a loop and introduced a subtle bug.

#include <iostream>
#include <vector>

// before: the loop started the running maximum at values[0]
int largest(const std::vector<int>& values) {
    int best = 0;                       // was values[0]
    for (int v : values) {
        if (v > best) best = v;
    }
    return best;
}

int main() {
    std::vector<int> negatives{ -5, -9, -3 };
    std::cout << largest(negatives) << '\n';    // wrong
    std::vector<int> mixed{ 4, 7, 2 };
    std::cout << largest(mixed) << '\n';        // still right
}

Task. Give the two printed values, and explain why the bug shows up on the first call but not the second. State the general lesson about extracting behaviour from a loop, and how a characterisation test (E151) would have caught this immediately.


E155 — Read the smells L4 analyze

A colleague sends a class for review. It has: a 300-line method, a bool isProcessed flag consulted in nine places, one parameter bool mode that changes behaviour in three branches, and a std::vector<std::string> temp used only inside that method but declared as a member.

Task. Name the smell each symptom represents, say what harm it causes, and propose the smallest concrete change that removes each one. One of the four is not worth fixing yet — say which and why.


E156 — Extract method, change meaning L4 debug scenario

#include <iostream>

class Accumulator {
public:
    int total() const { return total_; }

    // Before: total_ += n; if (total_ > 100) total_ = 100;
    void add(int n) {
        addRaw(n);
        clamp();
    }
private:
    void addRaw(int n) { total_ += n; }
    void clamp() { if (total_ > 100) total_ = 100; }
    int total_ = 0;
};

int main() {
    Accumulator a;
    a.add(60);
    a.add(70);
    std::cout << a.total() << '\n';
}

Task. This version is correct. Imagine the developer had instead written clamp() before addRaw(n). What would the program print then, and why is that a behaviour change rather than a safe refactoring? State the rule for extracting a method from a sequence of statements.


E157 — Design the capstone architecture L5 design scenario

You are building a multi-module equipment workshop system: it tracks equipment items, members who borrow them, loans with due dates, maintenance schedules, and it reports on all of it. It must persist its state to a file and be testable module by module.

Task. Design the application's structure on paper. Give:

  1. the modules (layers) and what each is responsible for, one line each;
  2. the dependency direction between them, and why it points that way;
  3. where the persistence lives, and why the domain types must not know about the file format;
  4. the interfaces (abstract types) that let a module be tested with a fake;
  5. one thing you would deliberately build last, and why.

How you will be judged. On the dependency direction and on testability — a design that cannot be tested module by module has not finished this exercise.


E158 — Review a design L5 design

A proposed design has: a Manager class that owns everything and is passed to every other class; a domain class with a save() method that writes SQL; and a single interface with eleven methods, most implementers leaving seven empty.

Task. Write a design review. For each of the three problems:

  1. name the principle(s) it violates;
  2. state the concrete risk it creates (not "it is ugly" — what goes wrong?);
  3. propose the corrected structure.

Then add one thing the design got right, and one question you would ask the author before changing anything. A review that finds only faults is not a review.


E159 — Legacy to OOP L6 refactor scenario

A 250-line procedural program stores equipment records in parallel arrays with free functions, and passes five parameters to almost everything. It works and is in daily use.

Task. Write a refactoring plan that moves it toward an object-oriented design without a rewrite. It must:

  1. state the characterisation tests you would write first, and what output they lock down;
  2. name the first type you would introduce and the smallest change that creates it;
  3. describe the sequence of small, always-green steps from arrays to types;
  4. name the riskiest step and how you would keep it reversible;
  5. state one thing you would not change, and why leaving it is the professional choice.

Success. Every step keeps the program working, and the plan could be executed by someone else.


E160 — Defend your design L6 design

Choose any design you produced for a design exercise in this bank (E80, E126, E137, E148 or E157), or the capstone you are building.

Task. Write a one-page design defence that answers, in order:

  1. the single responsibility of each type, in one sentence each;
  2. for every relationship, why it is composition, aggregation or association;
  3. which SOLID principle was hardest to satisfy, and how you satisfied it;
  4. one design decision you are least sure about, and what evidence would change your mind;
  5. one alternative design you considered and why you rejected it.

How you will be judged. On (4) and (5). A designer who claims no uncertainty has not examined their own work.


Previous topic: SOLID and design patterns · Bank home: The Exercise Bank · Hints: hints.md · Solutions: solutions.md