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

Комментарии (11)


  1. aamonster
    03.01.2024 15:22
    +3

    Умоляю, вычитывайте текст перед тем, как постить. Было больно, начиная с первого абзаца. "и как в следствии", "отвественности"...


  1. Kenya
    03.01.2024 15:22
    +5

    Разбудите меня через 100 лет, и спросите, чем занимаются разработчики на ООП-языках - я отвечу, что обсуждают принципы SOLID :)


    1. ALexKud
      03.01.2024 15:22

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


      1. Kenya
        03.01.2024 15:22

        Согласен, преждевременная оптимизация - очень частая боль. Я бы создавал точки возможного расширения для каких-то внешних взаимодействий (API, источники данных, экспорты импорты), а остальное рефакторить по мере поступления необходимости


  1. lamerok
    03.01.2024 15:22
    +10

    Вот представьте что в будущем к вам придёт администратор и скажет я хочу изменить его, чтоб он сохранял в Word файл (.doc), и вы измените его, но не заметите что этим же методом ещё и пользовался другой актор (Продукт менеджер). 

    Что-то не то, это же интерфейс, и его надо выделить отдельно для метода Save, а реализацию можете разную для разных акторов. Ексель для менеджера, и Ворд для администратора

    Не надо делать два одинаковых метода в разных интерфейса, которые по сути предназначены для одного и того же. Тем более, что совсем не очевидно иметь метод SaveToFile в интерфейсе IGetPlane.

    В общем сделали не совсем правильно.


    1. Kergan88
      03.01.2024 15:22

      В том и суть, что эти методы не одинаковые, у них в целом возможна разная сигнатура, т.к. разное, ни как не пересекающееся назначение. Если сделать один метод, то это уже прямое нарушение dry (ну или ssot, что то же самое).


      1. lamerok
        03.01.2024 15:22

        Если разная сигнатура, то и делайте разные методы с разными именами и отдельный интерфейс, в данном примере, это методы с одинаковой сигнатурой и предназначен для одной задачи - сохранить файл.

        У вас не метод один, а вот интерфейс должен быть один, при этом реализация разная может быть для разных акторов. Причём тут dry не понял.


  1. VladimirFarshatov
    03.01.2024 15:22

    Самое забавное тут то, что сам Мартин в своей же книге, ближе к концу, приводит несколько .. альтернативных примеров организации кода, и даже к простой программе указывает что-то типа ".. ну, такую простую фигню мы конечно напишем единым куском .. " в моем вольном переводе "по памяти".. не, не заходит. Ещё неплохо отыскать на Лурке описание принципов Solid .. очень наглядно, кмк. ;)

    Всех, с Наступившим Новым Годом!


  1. muryk
    03.01.2024 15:22

    С некоторым трепетом прочитал вашу статью. Скажите пожалуйста, в вашей организации есть открытые или архивные вакансии Flutter на HH.ру или подобных ресурсах? Если да, то не могли бы вы поделиться ссылкой? Можно в личку. Мне бы это сильно помогло откалибровать самооценку собственных умений и опыта работы, есть ощущение что я несколько оторвался от реальности. Спасибо


  1. Kuch
    03.01.2024 15:22

    В общем, я делаю вывод что кандидат не читал книгу "Чистая архитектура", либо это делал очень невнимательно...

    Я бы такого даже кандидатом не назвал. Чисто проходимец. И ведь не постеснялся с такими знаниями прийти на собеседование, ещё и на джуна


  1. audiserg
    03.01.2024 15:22

    Теперь глядя на класс Plane , совершенно не понятно что с ним делать. Он превратился в data class, а его использование придется искать по всему проекту. Тут напрашивается все же какая-то аггрегация в итоге, возможно некий класс медиатор, который имплементирует все интерфейсы и содержит их делегаты.