Правильное понимание Single Responsibility Principle (SRP) в Dart/Flutter

от автора

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


ссылка на оригинал статьи https://habr.com/ru/articles/784528/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *