Card 02/ 08

CodeDifficulty: Beginner2 min

Spotting a Single Responsibility Violation

A report class that calculates, formats and saves; a new format breaks the totals.

A single responsibility violation is visible in code once you ask, for each method, who would ask for it to change. This card shows a small SalesReport class in Python, Java and TypeScript with three reasons to change, and the highlighted lines mark where each one lives. All three versions print the same saved line.

A report class with three reasons to change

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

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

    def as_text(self):
        return f"TOTAL SALES: {self.total():.2f}"

    def save(self, store):
        store.append(self.as_text())


store = []
report = SalesReport([19.99, 5.01, 75.00])
report.save(store)
print(store)
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 SrpReport {
    public static void main(String[] args) {
        List<String> store = new ArrayList<>();
        SalesReport report = new SalesReport(new double[] {19.99, 5.01, 75.00});
        report.save(store);
        System.out.println(store);
    }
}

class SalesReport {
    private final double[] sales;

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

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

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

    void save(List<String> store) {
        store.add(asText());
    }
}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
[TOTAL SALES: 100.00]
ts
class SalesReport {
  constructor(private readonly sales: number[]) {}

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

  asText(): string {
    return `TOTAL SALES: ${this.total().toFixed(2)}`;
  }

  save(store: string[]): void {
    store.push(this.asText());
  }
}

const store: string[] = [];
const report = new SalesReport([19.99, 5.01, 75.0]);
report.save(store);
console.log(store);
OutputRan with node:22-slim (Node 22.23.1), TypeScript 5.9.3
[ 'TOTAL SALES: 100.00' ]

Read the class one method at a time and name the actor behind each:

  • total decides how sales are added up and rounded. Finance owns that rule.
  • as_text (asText in Java and TypeScript) decides how the figure looks. Marketing owns that.
  • save decides where the text goes. The platform team owns storage.

The three methods are tied together inside the class. save calls as_text, and as_text calls total. So a change to rounding changes what is formatted, and a change to the format changes what is saved. Each edit for one team runs through code the other teams depend on, and every test of the report has to deal with all three jobs at once.

Nothing here is broken today. The output is right. The violation is about what happens on the next change request, which is why it is easy to miss in a review: you have to ask who will ask for changes, not whether the code works.