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. Используя данный пример мы можем рассмотреть патерн Фасад в следующей статье.