SRP оказался самым сложным принципом из всех SOLID принципов в понимании и как следствие неправильное применение в кодировании. Множество разработчиков уровня junior / middle, которых я собеседовал на позицию Flutter разработчика давали ответ, что SRP - это принцип единой ответственности.
Это конечно правильный ответ согласно книги Роберта Мартина "Чистая архитектура". Но мне хотелось услышать как понимает этот принцип наш кандидат в разработчики. Ведь от этого зависит расширяемость и простота читаемости нашего проекта, ведь мы расширяем команду и хотелось бы чтоб мы писали код в единой концепции. В большинстве случаев разработчики понимают этот принцип, как класс, который он создал должен содержать только один метод. И всё что мы написали в этом методе, несёт единственную ответственность, ведь он решает одну задачу. И на этом кандидат заканчивает свою мысль.
Ну что же, неплохо, но и не совсем правильно. Да, такой принцип тоже есть, но он применяется на низшем уровне системы. Действительно для удобства чтения не надо всё сваливать в один метод. Практичнее будет если мы каждому методу разрешим делать что то одно и название этого метода будет понятно другому разработчику, что делает этот метод. SRP же применяется на среднем уровне программы. В общем, я делаю вывод что кандидат не читал книгу "Чистая архитектура", либо это делал очень невнимательно...
SRP это про другое! Сам Роберт Мартин столкнулся с такой же проблемой, неправильное понимание того, что он имел ввиду под SRP и попробовал её решить, написав другое определение и разъяснение к нему. Давайте почитаем!

Обратите внимание, есть определение что такое модуль и кто такие акторы! Спасибо дядюшка Боб, теперь нам стало жить легче!
Давайте теперь попробуем разобрать это на примере. Я буду использовать язык Dart.
Чтоб понять, как решить проблему SRP надо её сначала создать. Итак мы написали класс Plane (Самолёт), в котором есть данные (поля модель, кол-во пассажиров, и какие он выполняет рейсы, коммерческие или нет. ) и методы. Теперь мы знаем, что класс это и есть тот самый модуль.

Получилось вот такая портянка. С методами я решил не заморачиваться, чтоб не раздувать код. Итак мы видим что в одном классе лежат данные и методы, у которых разные акторы и это проблема. Это нарушает принцип SRP. Давайте теперь решим эту проблему.
Если вы обратили внимание я специально обозначил акторов над каждым методом. То есть я, как разработчик, должен подумать для кого я делаю этот метод, тот скорее всего и будет желать изменений в этом методе. Ну что же давайте править. Наша задача раскидать методы по новым классам, это будут классы закреплённые за конкретным актором. А так же сделаем интерфейсы к ним, как говорится в лучших традициях разработки.
// Финансовый директор. interface class IExport { void exportToExel() {} void exportToWord() {} }
Интерфейс для Актора - Финансовый директор, и тут не обязательно что это один человек, возможно это весь финансовый департамент, то есть группа лиц
// Продукт менеджер. interface class IGetPlane { void getListPlane() {} void saveToFile() {} }
Актор - Продукт менеджер, который желает получать список самолётов и сохранять их в файл
// Пользователь. interface class ILoad { void loadPlane() {} }
Интерфейс для пользователя, который может загрузить (не важно куда) свой самолёт
// Тестировщик. interface class ILogger { void logger() {} }
Интерфейс для тестировщика, он тоже хочет тестить приложение да ещё таким образом чтоб велись логи.
// Администратор БД. interface class ISave { void saveToDB() {} void saveToFile() {} }
Ну и конечно про Администратора не забудем, он будет желать изменений в этих методах в будущем
Обратите внимание!
Был метод saveToFile в котором были заинтересованы 2 актора. Продукт менеджер и администратор БД. Один и тот же метод мы задублировали и разнесли по разным классам. Выглядит это плохо! Но только с первого взгляда. Вот представьте что в будущем к вам придёт администратор и скажет я хочу изменить его, чтоб он сохранял в Word файл (.doc), и вы измените его, но не заметите что этим же методом ещё и пользовался другой актор (Продукт менеджер). И вот теперь ваш общий метод сохраняет в Word. К вам приходит Продукт менеджер и говорит, у меня не стоит Word, мне нужно чтоб сохранялось в Excel (.xls). И вот привет! Конфликт интересов. То есть сделали мы всё таки правильно!
Теперь создадим имплементации наших интерфейсных классов.
// Финансовый директор. class ExportImpl implements IExport { final Plane plane; const ExportImpl({required this.plane}); @override void exportToExel() { print('Экспорт в Эксель ${plane.model}'); } @override void exportToWord() { print('Экспорт в Ворд'); } }
Имплементация класса для актора - финансовый директор
// Продукт менеджер. class GetPlaneImpl implements IGetPlane { @override void getListPlane() { print('получить список самолетов'); } @override void saveToFile() { print('Сохранение в файл'); } }
Так же имплементация интерфейса для Продукт менеджера
// Пользователь. class LoadPlaneImpl implements ILoad { @override void loadPlane() { print('Загрузка самолёта пользователем'); } }
Имплеменитруем класс для Пользователя
// Тестировщик. class LoggerImpl implements ILogger { void logger() { print('логирование'); } }
Аналогично напишем реализацию и для актора - Тестировщика, он будет очень доволен что про него не забыли.
// Администратор БД. class SaveImpl implements ISave { final Plane plane; const SaveImpl({required this.plane}); @override void saveToDB() { print('сохранить в базу SQL'); } @override void saveToFile() { print('Сохранение в файл'); } }
Ну и последний класс для актора это администратор БД.
Итак мы все методы разнесли согласно нашим заинтересованным лицам. И наш класс Plane остался только с данными, которые мы пробросим в метод через конструктор.
class Plane { final String model; final int numberOfPassangers; final bool isCommerce; const Plane({ required this.model, required this.numberOfPassangers, required this.isCommerce, }); }
Так теперь выглядит наш класс Plane. Ничего лишнего.
Теперь давайте перейдём в main и запустим код
void main() { Plane plane = const Plane( model: 'Cesna 172', numberOfPassangers: 4, isCommerce: false, ); final export = ExportImpl(plane: plane); export.exportToExel(); }
Мы создали инстанс класса Plane с данными и ExportImpl (актор - финансовый директор), прокинув туда через конструктор данные о самолёте

Видим в консоли наш принт. Можем сделать вывод что всё работает, как надо!
Таким образом мы решили проблему несоответствия SRP. Используя данный пример мы можем рассмотреть патерн Фасад в следующей статье.
