Pull to refresh

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

Level of difficultyMedium
Reading time4 min
Views4.8K

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

Tags:
Hubs:
Total votes 9: ↑4 and ↓5+3
Comments13

Articles