TODO: отвалидировать – текст собран автоматически из материалов курса на Google Drive, проверьте формулировки и код.
Класс должен иметь только одну причину для изменения. Иными словами: один класс – одна обязанность.
Если объект делает слишком много, его сложнее поддерживать и изменять, а правка в одной части может сломать другую.
Причина для изменения, а не количество методов
Формулировку часто понимают как «класс должен быть маленьким». Это не так: важна не длина, а источник изменений. Спросите себя, кто может прийти с требованием поменять этот класс. Если таких заказчиков двое – бухгалтерия и отдел вёрстки писем, – обязанностей тоже две.
// Две причины меняться: правила расчёта зарплаты и формат отчёта
public class Employee
{
public decimal CalculatePay() => 0;
public string ExportToCsv() => "";
}
// Каждая причина живёт в своём классе
public class Employee
{
public decimal CalculatePay() => 0;
}
public class EmployeeCsvExporter
{
public string Export(Employee employee) => "";
}
// Две причины меняться: правила расчёта зарплаты и формат отчёта
class Employee {
public:
double CalculatePay() const { return 0; }
std::string ExportToCsv() const { return ""; }
};
// Каждая причина живёт в своём классе
class Employee {
public:
double CalculatePay() const { return 0; }
};
class EmployeeCsvExporter {
public:
std::string Export(const Employee& employee) const { return ""; }
};
// Две причины меняться: правила расчёта зарплаты и формат отчёта
type Employee struct{}
func (Employee) CalculatePay() float64 { return 0 }
func (Employee) ExportToCSV() string { return "" }
// Каждая причина живёт в своём типе
type Employee struct{}
func (Employee) CalculatePay() float64 { return 0 }
type EmployeeCSVExporter struct{}
func (EmployeeCSVExporter) Export(e Employee) string { return "" }
# Две причины меняться: правила расчёта зарплаты и формат отчёта
class Employee:
def calculate_pay(self) -> float:
return 0
def export_to_csv(self) -> str:
return ""
# Каждая причина живёт в своём классе
class Employee:
def calculate_pay(self) -> float:
return 0
class EmployeeCsvExporter:
def export(self, employee: Employee) -> str:
return ""
// Две причины меняться: правила расчёта зарплаты и формат отчёта
class Employee {
calculatePay(): number {
return 0;
}
exportToCsv(): string {
return "";
}
}
// Каждая причина живёт в своём классе
class Employee {
calculatePay(): number {
return 0;
}
}
class EmployeeCsvExporter {
export(employee: Employee): string {
return "";
}
}
// Две причины меняться: правила расчёта зарплаты и формат отчёта
public class Employee {
public BigDecimal calculatePay() {
return BigDecimal.ZERO;
}
public String exportToCsv() {
return "";
}
}
// Каждая причина живёт в своём классе
public class Employee {
public BigDecimal calculatePay() {
return BigDecimal.ZERO;
}
}
public class EmployeeCsvExporter {
public String export(Employee employee) {
return "";
}
}
// Две причины меняться: правила расчёта зарплаты и формат отчёта
class Employee {
fun calculatePay(): BigDecimal = BigDecimal.ZERO
fun exportToCsv(): String = ""
}
// Каждая причина живёт в своём классе
class Employee {
fun calculatePay(): BigDecimal = BigDecimal.ZERO
}
class EmployeeCsvExporter {
fun export(employee: Employee): String = ""
}
Что даёт соблюдение
- Изменение требования затрагивает один файл. Поменялись правила начисления зарплаты – правится расчёт, и только он; формат отчёта об этом не знает.
- Тест становится осмысленным. У класса одна причина упасть, поэтому по красному тесту сразу понятно, что именно сломалось.
- Меньше конфликтов при слиянии. Бухгалтерия и отдел отчётности правят разные файлы, а не соседние строки одного класса.
- Класс легко назвать. Если имя подбирается без союза «и» – ответственность одна. Это самая дешёвая проверка принципа, и работает она ещё до написания кода.
Чем платят
Классов становится больше, и по одному сценарию приходится ходить по нескольким файлам. Принцип окупается там, где код действительно меняется; дробить стабильный класс ради самого дробления смысла нет.