Card 02/ 08
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
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)['TOTAL SALES: 100.00']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());
}
}[TOTAL SALES: 100.00]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);[ 'TOTAL SALES: 100.00' ]Read the class one method at a time and name the actor behind each:
totaldecides how sales are added up and rounded. Finance owns that rule.as_text(asTextin Java and TypeScript) decides how the figure looks. Marketing owns that.savedecides 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.
← → move through sections and cards