Card 03/ 08

WalkthroughDifficulty: Beginner3 min

Refactoring to Single Responsibility, Step by Step

Pull out the formatter, then the store, and leave the calculation where it is.

Refactoring to single responsibility means moving each reason to change into its own class, one at a time, while the program keeps printing exactly what it printed before. This card runs that process on the SalesReport class, whose total, as_text and save methods answer to finance, marketing and the platform team.

The order matters. Extract the parts that depend on the others first, and leave the part everything depends on, the calculation, for last. Check the output after every step, so that a mistake is caught in the step that made it.

Splitting SalesReport into three classes

Pin the output

Run the original class and note what it prints: ['TOTAL SALES: 100.00']. That line is the test for every step that follows. In a real codebase this is a unit test, written before anything moves.

Extract the formatter

Create a TextFormatter class with one method, format(total), holding the body of as_text. Change as_text to call it. Run again; the output must be the same. Marketing's requests now land in TextFormatter only.

Extract the store

Create a ReportStore class that is given the list of rows in its constructor and has a save(text) method. Move the append into it. The caller now asks the formatter for the text and hands it to the store. Run again.

Rename what is left

Only the calculation remains in the old class, so its name should say so. Rename SalesReport to SalesTotal and total to value. Delete the now-empty as_text and save. Run one last time.

The structure before and after:

SalesReport before the split, and the three classes after it

The final code, in each language, still prints the same line:

The three classes after the split

py
class SalesTotal:
    def __init__(self, sales):
        self.sales = sales

    def value(self):
        return round(sum(self.sales), 2)


class TextFormatter:
    def format(self, total):
        return f"TOTAL SALES: {total:.2f}"


class ReportStore:
    def __init__(self, rows):
        self.rows = rows

    def save(self, text):
        self.rows.append(text)


rows = []
total = SalesTotal([19.99, 5.01, 75.00]).value()
ReportStore(rows).save(TextFormatter().format(total))
print(rows)
OutputRan with python:3.12-slim (Python 3.12.13)
['TOTAL SALES: 100.00']
java
import java.util.ArrayList;
import java.util.List;

public class SrpSplit {
    public static void main(String[] args) {
        List<String> rows = new ArrayList<>();
        double total = new SalesTotal(new double[] {19.99, 5.01, 75.00}).value();
        new ReportStore(rows).save(new TextFormatter().format(total));
        System.out.println(rows);
    }
}

class SalesTotal {
    private final double[] sales;

    SalesTotal(double[] sales) {
        this.sales = sales;
    }

    double value() {
        double sum = 0;
        for (double sale : sales) sum += sale;
        return Math.round(sum * 100) / 100.0;
    }
}

class TextFormatter {
    String format(double total) {
        return String.format("TOTAL SALES: %.2f", total);
    }
}

class ReportStore {
    private final List<String> rows;

    ReportStore(List<String> rows) {
        this.rows = rows;
    }

    void save(String text) {
        rows.add(text);
    }
}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
[TOTAL SALES: 100.00]
ts
class SalesTotal {
  constructor(private readonly sales: number[]) {}

  value(): number {
    const sum = this.sales.reduce((a, b) => a + b, 0);
    return Math.round(sum * 100) / 100;
  }
}

class TextFormatter {
  format(total: number): string {
    return `TOTAL SALES: ${total.toFixed(2)}`;
  }
}

class ReportStore {
  constructor(private readonly rows: string[]) {}

  save(text: string): void {
    this.rows.push(text);
  }
}

const rows: string[] = [];
const total = new SalesTotal([19.99, 5.01, 75.0]).value();
new ReportStore(rows).save(new TextFormatter().format(total));
console.log(rows);
OutputRan with node:22-slim (Node 22.23.1), TypeScript 5.9.3
[ 'TOTAL SALES: 100.00' ]

Each class now answers to one team. Finance edits SalesTotal, marketing edits TextFormatter, and the platform team edits ReportStore. None of them calls the others. The caller at the bottom wires them together, and that wiring is the only place that knows all three exist.

The split also makes testing simpler. The rounding rule can be tested with a list of numbers and nothing else, and the formatter with one number. Neither test needs a store.