Course program← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF

SRP – the Single Responsibility Principle

TODO: validate – translated from the Russian original, please review the wording.

A class should have only one reason to change. In other words: one class, one responsibility.

When an object does too much, it becomes harder to maintain and modify, and a change in one part can break another.

A reason to change, not a number of methods

The principle is often read as «a class should be small». It is not: what matters is not the length but the source of change. Ask yourself who could come asking to modify this class. If there are two such people – accounting and the team that formats emails – there are two responsibilities.

// Two reasons to change: payroll rules and the report format
public class Employee
{
    public decimal CalculatePay() => 0;
    public string ExportToCsv() => "";
}

// Each reason lives in its own class
public class Employee
{
    public decimal CalculatePay() => 0;
}

public class EmployeeCsvExporter
{
    public string Export(Employee employee) => "";
}
// Two reasons to change: payroll rules and the report format
class Employee {
public:
    double CalculatePay() const { return 0; }
    std::string ExportToCsv() const { return ""; }
};

// Each reason lives in its own class
class Employee {
public:
    double CalculatePay() const { return 0; }
};

class EmployeeCsvExporter {
public:
    std::string Export(const Employee& employee) const { return ""; }
};
// Two reasons to change: payroll rules and the report format
type Employee struct{}

func (Employee) CalculatePay() float64 { return 0 }
func (Employee) ExportToCSV() string   { return "" }

// Each reason lives in its own type
type Employee struct{}

func (Employee) CalculatePay() float64 { return 0 }

type EmployeeCSVExporter struct{}

func (EmployeeCSVExporter) Export(e Employee) string { return "" }
# Two reasons to change: payroll rules and the report format
class Employee:
    def calculate_pay(self) -> float:
        return 0

    def export_to_csv(self) -> str:
        return ""


# Each reason lives in its own class
class Employee:
    def calculate_pay(self) -> float:
        return 0


class EmployeeCsvExporter:
    def export(self, employee: Employee) -> str:
        return ""
// Two reasons to change: payroll rules and the report format
class Employee {
    calculatePay(): number {
        return 0;
    }

    exportToCsv(): string {
        return "";
    }
}

// Each reason lives in its own class
class Employee {
    calculatePay(): number {
        return 0;
    }
}

class EmployeeCsvExporter {
    export(employee: Employee): string {
        return "";
    }
}
// Two reasons to change: payroll rules and the report format
public class Employee {
    public BigDecimal calculatePay() {
        return BigDecimal.ZERO;
    }

    public String exportToCsv() {
        return "";
    }
}

// Each reason lives in its own class
public class Employee {
    public BigDecimal calculatePay() {
        return BigDecimal.ZERO;
    }
}

public class EmployeeCsvExporter {
    public String export(Employee employee) {
        return "";
    }
}
// Two reasons to change: payroll rules and the report format
class Employee {
    fun calculatePay(): BigDecimal = BigDecimal.ZERO
    fun exportToCsv(): String = ""
}

// Each reason lives in its own class
class Employee {
    fun calculatePay(): BigDecimal = BigDecimal.ZERO
}

class EmployeeCsvExporter {
    fun export(employee: Employee): String = ""
}

What following it buys you

  • A changed requirement touches one file. Payroll rules changed – you edit the payroll calculation and nothing else; the report format never hears about it.
  • A failing test becomes meaningful. The class has one reason to fail, so a red test tells you immediately what broke.
  • Fewer merge conflicts. Accounting and reporting edit different files instead of neighbouring lines in one class.
  • The class is easy to name. If the name comes without the word «and», the responsibility is single. That is the cheapest check available, and it works before a single line is written.

What it costs

You end up with more classes, and following one scenario means walking through several files. The principle pays off where the code actually changes; splitting a stable class for the sake of splitting buys nothing.