Принцип единственной ответственности SRP в SOLID

Принцип единственной ответственности (Single Responsibility Principle, SRP) - первая и самая обманчиво простая буква в аббревиатуре SOLID. Формулировка короткая: у класса должна быть только одна причина для изменения. Но за этой фразой стоит целая дисциплина мышления о том, что считать «ответственностью» и как не превратить один божественный класс в неуправляемый ком кода. Ниже разберём точную формулировку Роберта Мартина, научимся считать «причины для изменения» и пройдём типичный рефакторинг - а интерактивный разбор ниже соберёт запрос под ваш конкретный класс.
Что такое SRP и кто его сформулировал
SRP - один из пяти принципов SOLID, набора рекомендаций по объектно-ориентированному проектированию, который популяризировал Роберт Мартин (Uncle Bob) в начале 2000-х. Сама идея старше: она вырастает из понятий связности (cohesion) и зацепления (coupling), описанных ещё Ларри Константайном и Эдвардом Йурданом в 1970-х.
Классическая формулировка SRP звучит так: «A class should have only one reason to change» - у класса должна быть только одна причина для изменения. Ключевое слово здесь - причина, а не действие. Класс может делать много мелких вещей, но все они должны служить одной зоне ответственности.
Позже Мартин уточнил формулировку, чтобы убрать двусмысленность слова «причина»: модуль должен отвечать перед одним и только одним актором (actor) - одной группой заинтересованных лиц, которая может потребовать изменений. Бухгалтерия, отдел кадров и администратор базы - это три разных актора, и логика, которая меняется по их запросам, не должна жить в одном классе.

Что считать ответственностью
Главная сложность SRP - определить границу ответственности. Слишком крупная гранулярность («класс отвечает за заказы») оставляет внутри несколько причин для изменения; слишком мелкая («класс хранит одно поле») плодит анемичные классы и рассеивает логику.
Рабочий критерий - связать ответственность с актором и с осью изменений. Спросите себя: кто и почему придёт с требованием переписать этот код? Если ответов несколько и они исходят от разных людей или подсистем, перед вами кандидат на разделение.
Классический антипример - класс Employee с тремя методами:
calculatePay()- расчёт зарплаты, меняется по требованию бухгалтерии;reportHours()- отчёт об отработанных часах, меняется по требованию отдела кадров;save()- сохранение в базу, меняется по требованию администратора БД.
Три актора - три причины для изменения, значит, три ответственности в одном классе. Изменение формулы зарплаты рискует случайно сломать отчётность, потому что обе функции делят общий приватный метод подсчёта часов. Это и есть нарушение SRP.
Как посчитать причины для изменения
Удобный способ оценить класс - буквально перечислить акторов, перед которыми он отвечает, и оси, по которым его придётся менять. Каждая независимая ось - это отдельная причина для изменения.
Если обозначить число независимых причин для изменения как , то здоровый по SRP класс имеет . Связность класса можно грубо оценить отношением методов, работающих с общим состоянием, к общему числу методов: чем ближе оно к единице, тем выше шанс, что у класса действительно одна ответственность.
Это не математически строгая метрика, а инструмент рассуждения. Интерактивный разбор выше как раз помогает разложить ваш класс на отдельные оси изменений и понять, нужно ли его дробить. SRP - лишь первый из пяти принципов SOLID с примерами, и считать «причины для изменения» полезно при работе с каждым из них.
Рефакторинг: разделяем ответственности
Вернёмся к классу Employee. Рефакторинг по SRP разносит три ответственности по трём классам, оставляя Employee чистой структурой данных:
PayCalculator- отвечает только за расчёт зарплаты;HourReporter- отвечает только за отчёт о часах;EmployeeRepository- отвечает только за сохранение и загрузку.
Общий приватный метод подсчёта часов выносится в отдельный класс HourCalculator, от которого зависят и PayCalculator, и HourReporter - теперь изменение одной функции не задевает другую. Этот приём Мартин называет паттерном Facade или Interactor: тонкий координатор склеивает специализированные классы, не зная их внутренностей.

После разделения каждый класс получает одну причину для изменения, легче тестируется (мок одного актора, а не всего) и переиспользуется независимо.
SRP и связность: две стороны одной идеи
SRP тесно связан с понятием связности (cohesion). Высокая связность означает, что все элементы класса работают над одной задачей. Низкая связность - признак того, что класс собрал в кучу несвязанные обязанности.
Можно сказать, что SRP - это требование максимальной функциональной связности: каждый метод и каждое поле класса должны служить единственной ответственности. Когда вы замечаете, что поля класса используются разными группами методов, которые между собой почти не пересекаются, перед вами две скрытые ответственности, ждущие разделения.
SRP на уровне функций и микросервисов
Хотя формулировка говорит о классе, тот же принцип масштабируется вверх и вниз:
- Функция должна делать одну вещь - это прямое следствие SRP на уровне процедур.
- Модуль или пакет должен группировать классы, меняющиеся по одной причине.
- Микросервис в идеале владеет одной бизнес-возможностью - это SRP, поднятый до архитектурного уровня (близко к понятию bounded context из DDD).
Единая логика на всех уровнях: чем меньше причин для изменения у единицы кода, тем устойчивее система к каскадным правкам.
Чем SRP не является
- SRP - не про «один метод на класс». Класс может иметь десятки методов, если все они служат одной ответственности и меняются по одной причине.
- SRP - не про размер файла. Маленький класс может нарушать SRP (две причины для изменения в двадцати строках), а большой - соблюдать.
- SRP - не про слои. Разделение на контроллер, сервис и репозиторий - следствие SRP, но механическое нарезание по слоям без анализа акторов не гарантирует его соблюдения.
Частые ошибки
- Путают «причину» с «действием». SRP не запрещает классу делать несколько действий - он запрещает несколько причин для изменения. Считайте акторов, а не методы.
- Дробят до анемичности. Слишком ревностное применение SRP плодит сотни классов-однострочников, и логика рассеивается. Граница ответственности - там, где меняется актор, а не каждое поле.
- Игнорируют скрытое разделение состояния. Два метода, делящие общий приватный код, кажутся одной ответственностью, но если их меняют разные акторы - это нарушение, замаскированное переиспользованием.
- Считают, что слои = SRP. Нарезка по слоям без анализа акторов оставляет несколько причин для изменения внутри одного «сервиса».
- Применяют SRP до того, как появилась боль. Преждевременное разделение усложняет код без выгоды. Дробите, когда видите реальную ось изменений, а не гипотетическую.
FAQ
В чём разница между SRP и принципом разделения интерфейсов (ISP)? SRP говорит о причинах изменения класса (один актор - одна ответственность), а ISP - о том, что клиент не должен зависеть от методов, которые не использует. SRP про внутреннюю связность класса, ISP про гранулярность интерфейсов. Они дополняют друг друга, но решают разные задачи.
Как SRP связан с тестируемостью? Класс с одной ответственностью тестируется проще: у него меньше зависимостей и один сценарий поведения. Когда в классе слиты три ответственности, тест вынужден поднимать всё окружение сразу, а изменение одной части ломает тесты другой. Разделение по SRP делает юнит-тесты узкими и стабильными.
Обязательно ли соблюдать SRP всегда? Нет, SRP - рекомендация, а не закон. На ранних стадиях прототипа или в простом скрипте строгое разделение бывает излишним. Применяйте его, когда класс начинает меняться по нескольким причинам и эти изменения мешают друг другу - то есть когда нарушение приносит реальную боль.
Коротко
Принцип единственной ответственности SRP требует, чтобы у класса была одна и только одна причина для изменения, то есть он отвечал перед одним актором. «Ответственность» определяется не через действия, а через группу заинтересованных лиц, способную потребовать правок. Классический пример нарушения - класс Employee с расчётом зарплаты, отчётностью и сохранением: три актора, три причины. Рефакторинг разносит ответственности по специализированным классам, повышая связность, тестируемость и устойчивость к каскадным изменениям. Главное - считать причины для изменения, а не методы, и дробить тогда, когда нарушение приносит реальную боль, а не на всякий случай.
Читайте также

Принципы SOLID с примерами: разбор пяти правил ООП
Принципы SOLID с примерами на коде: расшифровка SRP, OCP, LSP, ISP и DIP, типичные нарушения и рефакторинг, понятные аналогии и ответы для собеседования.

Принцип инверсии зависимостей DIP: зависим от абстракций
Принцип инверсии зависимостей (DIP) из SOLID простыми словами: почему модули верхнего уровня не должны зависеть от деталей, как развернуть стрелку зависимости через интерфейс и внедрение зависимостей.

Принцип подстановки Барбары Лисков (LSP): суть и примеры
Принцип подстановки Барбары Лисков (LSP): что значит подстановочность подтипов, контракты, ковариантность, пример Квадрат-Прямоугольник и как не нарушать букву L в SOLID.