Card 04/ 08

ExampleDifficulty: Intermediate2 min

Single Responsibility Example: The Employee Class With Three Bosses

Payroll and HR share a helper, and a change for one corrupts the other's numbers.

Two actors sharing one class lets a change for one of them silently corrupt the results of the other, and this example shows it happening. It is the case Robert C. Martin uses in Clean Architecture: an Employee class whose pay calculation belongs to the finance department and whose hours report belongs to HR. Both methods happen to call the same helper, regular_hours, because today both departments count the first forty hours of a week as regular.

py
class Employee:
    def __init__(self, name, hours, rate):
        self.name = name
        self.hours = hours
        self.rate = rate

    def regular_hours(self):
        return min(self.hours, 40)

    def calculate_pay(self):
        return self.regular_hours() * self.rate

    def report_hours(self):
        return f"{self.name}: {self.regular_hours()} regular hours"


sam = Employee("Sam", 45, 20)
print(f"payroll sees: {sam.calculate_pay()}")
print(f"HR sees:      {sam.report_hours()}")
OutputRan with python:3.12-slim (Python 3.12.13)
payroll sees: 800
HR sees:      Sam: 40 regular hours
Payroll and HR both rely on regular_hours.

Payroll now negotiates a new contract: overtime starts after 37.5 hours. A developer on the payroll project finds regular_hours, changes 40 to 37.5, checks that the pay comes out right, and ships. HR's rule has not changed, and nobody tells HR.

py
class Employee:
    def __init__(self, name, hours, rate):
        self.name = name
        self.hours = hours
        self.rate = rate

    def regular_hours(self):
        return min(self.hours, 37.5)

    def calculate_pay(self):
        return self.regular_hours() * self.rate

    def report_hours(self):
        return f"{self.name}: {self.regular_hours()} regular hours"


sam = Employee("Sam", 45, 20)
print(f"payroll sees: {sam.calculate_pay()}")
print(f"HR sees:      {sam.report_hours()}")
OutputRan with python:3.12-slim (Python 3.12.13)
payroll sees: 750.0
HR sees:      Sam: 37.5 regular hours
One number changed for payroll, and both outputs moved.

Payroll's number is now what payroll wanted. HR's report now says 37.5 regular hours, which is wrong for HR, and no test for HR failed, because the change was made in a method that looked like payroll's.

The cause is not a careless developer. The two actors share code because their rules were identical for a while, and that coincidence is called accidental duplication: two pieces of logic that look the same but change for different reasons. Merging them into one helper couples two actors.

The single responsibility fix is to give each actor its own class: a PayCalculator with payroll's definition of regular hours, and an HourReporter with HR's. The duplicated three-line helper is the price, and it is cheap. When the rules diverge, each team edits only its own copy.