Что такое принцип единственной ответственности (Single Responsibility Principle, SRP)?

Ответ

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

Что это значит на практике? Класс должен быть ответственен за одну узкую часть функциональности (задачу, аспект). Если у класса несколько несвязанных обязанностей, то изменения в требованиях к одной из них будут вынуждать менять этот класс, рискуя сломать другую.

Пример класса, нарушающего SRP:

public class OrderProcessor
{
    // Ответственность 1: Валидация заказа
    public bool Validate(Order order) { /* ... */ }

    // Ответственность 2: Сохранение в БД
    public void SaveToDatabase(Order order) { /* ... */ }

    // Ответственность 3: Генерация чека
    public void GenerateInvoice(Order order) { /* ... */ }

    // Ответственность 4: Отправка уведомления
    public void SendEmailConfirmation(Order order) { /* ... */ }
}

Такой "божественный объект" (God Object) сложно тестировать, повторно использовать и поддерживать. Изменение формата email затронет класс, отвечающий за логику заказов.

Рефакторинг с соблюдением SRP:

// Класс отвечает только за бизнес-логику обработки заказа
public class OrderService
{
    private readonly IOrderValidator _validator;
    private readonly IOrderRepository _repository;
    private readonly IInvoiceGenerator _invoiceGenerator;
    private readonly INotificationService _notificationService;

    // Зависимости внедряются через конструктор (Dependency Injection)
    public OrderService(IOrderValidator validator,
                        IOrderRepository repository,
                        IInvoiceGenerator invoiceGenerator,
                        INotificationService notificationService)
    {
        _validator = validator;
        _repository = repository;
        _invoiceGenerator = invoiceGenerator;
        _notificationService = notificationService;
    }

    public void ProcessOrder(Order order)
    {
        if (!_validator.Validate(order))
            throw new ValidationException("Invalid order");

        _repository.Save(order);
        _invoiceGenerator.Generate(order);
        _notificationService.SendConfirmation(order);
    }
}

// Каждый сервис отвечает за свою узкую задачу
public interface IOrderValidator { bool Validate(Order order); }
public interface IOrderRepository { void Save(Order order); }
public interface IInvoiceGenerator { void Generate(Order order); }
public interface INotificationService { void SendConfirmation(Order order); }

Преимущества SRP:

  • Упрощение тестирования: Каждый маленький класс проще покрыть unit-тестами.
  • Повышение переиспользуемости: Класс EmailNotificationService можно использовать не только для заказов.
  • Снижение связанности: Изменения в логике отправки email изолированы в одном классе.
  • Улучшение читаемости: Код организован по четким, понятным модулям.

SRP применим не только к классам, но и к методам (метод должен делать одну вещь) и даже к модулям в целом.

Ответ 18+ 🔞

Давай разберём этот ваш принцип единственной ответственности, а то народ часто его так понимает, что потом сам себя в угол загоняет.

Представь себе мужика, который и печки кладёт, и сантехнику чинит, и на гитаре играет. Вроде мастер на все руки, а на деле — хрен пойми что. Позвал ты его унитаз починить, а он тебе вдруг печку в ванной начинает перекладывать, потому что ему так удобнее. Итог: унитаз течёт, печка дымит, а ты сидишь в сортире и думаешь, какого хуя так вышло.

Вот примерно так же выглядит класс, который нарушает SRP. Он пытается делать всё сразу, а в итоге делает всё через жопу.

Смотри, вот тебе пример, прямо классика жанра:

public class OrderProcessor
{
    public bool Validate(Order order) { /* ... */ }
    public void SaveToDatabase(Order order) { /* ... */ }
    public void GenerateInvoice(Order order) { /* ... */ }
    public void SendEmailConfirmation(Order order) { /* ... */ }
}

Это что за универсальный солдат? Он и валидирует, и в базу пихает, и счета генерит, и письма рассылает. У него причин для изменения — овердохуища. Захотел бухгалтер поменять формат счёта — придётся лезть в этот класс и рисковать сломать отправку писем. Решил админ перейти с MySQL на PostgreSQL — опять тут ковыряться, и заодно можешь по ошибке накосячить с валидацией. Короче, пиздец, а не класс. Тестировать его — отдельный ад, потому что он завязан на всё подряд.

А теперь смотри, как надо бы, по-человечески:

public class OrderService
{
    private readonly IOrderValidator _validator;
    private readonly IOrderRepository _repository;
    private readonly IInvoiceGenerator _invoiceGenerator;
    private readonly INotificationService _notificationService;

    public OrderService(IOrderValidator validator,
                        IOrderRepository repository,
                        IInvoiceGenerator invoiceGenerator,
                        INotificationService notificationService)
    {
        _validator = validator;
        _repository = repository;
        _invoiceGenerator = invoiceGenerator;
        _notificationService = notificationService;
    }

    public void ProcessOrder(Order order)
    {
        if (!_validator.Validate(order))
            throw new ValidationException("Invalid order");

        _repository.Save(order);
        _invoiceGenerator.Generate(order);
        _notificationService.SendConfirmation(order);
    }
}

Видишь разницу? Теперь у нас каждый занимается своим делом. Один чувак только валидирует, другой — только в базу тыкает, третий — счета штампует, четвёртый — письмами грузит. А OrderService просто главный по тарелкам — он всех этих узких специалистов координирует.

И что это даёт, спросишь?

  1. Тестировать — одно удовольствие. Хочешь проверить логику отправки писем? Подсовываешь заглушку для репозитория и валидатора, и тестируй на здоровье. Ничего лишнего не мешается.
  2. Переиспользовать можно. Класс EmailNotificationService теперь не привязан к заказам. Захотел — используй его для уведомлений о регистрации пользователя. И не надо ничего выковыривать из старого монстра.
  3. Менять — не страшно. Поменялось требование по формату письма? Идешь в один единственный класс EmailNotificationService и правишь там. Остальной код даже не узнает, что что-то поменялось. Риск сломать что-то соседнее — ноль ебать.
  4. Читать — понятно. Открываешь класс и сразу видишь — а, этот за валидацию, этот за персистентность. Не надо полчаса в дебрях кода блуждать.

И да, этот принцип не только к классам относится. Метод тоже должен делать что-то одно. Не надо в одном методе и данные парсить, и в базу писать, и логгировать, и ещё кофе варить. Раздели на несколько мелких — и жить станет проще.

Короче, смысл в том, чтобы не создавать этих "швейцаровских ножей", которые в теории всё умеют, а на практике — ни хуя нормально не работают. Лучше десять маленьких, понятных классов, чем один большой, который всех бесит.

Видео-ответы