Оптимизация архитектуры: делим крупные классы с помощью миксинов

от автора

Всем привет! Это статья для тех, кто интересуется фреймворком Flutter и языком Dart. На связи Николай Омётов, руководитель Flutter разработки IT-компании Mad Brains. Сегодня обсудим, как делить большой файл класса с помощью миксинов. 

Для начала давайте разберемся, зачем вообще делить большой файл класса?

Причин несколько: во-первых, чтобы было удобно читать и рефакторить код. Во-вторых, чтобы уменьшить количество конфликтов при merge и разделении истории git в конкретном файле. Согласитесь, отследить историю изменений в разных маленьких файлах удобнее и быстрее, чем в одном большом, с разнообразием изменений. 

Рассмотрим пример проблемы?

Для общения с сервером у нас используется класс, реализующий RestService-интерфейс.

abstract interface class RestService{   const RestService();      String getProfileData();   void logout();   // ... и ещё много методов, пускай 52 } 

Само по себе это выглядит «почти» нормальным (об этом далее). Как пример, разработчик взял всего 52 строки с описанием сигнатур функций, но дальше реализация каждой функции будет занимать все больше и больше места:

class RestServiceMock implements RestService{   const RestServiceMock();      @override   void logout() => print('logout mock');      @override   String getProfileData() => 'getProfileData mock';      // На каждый метод уже 2 строки } 
А в реализации мы уже видим неконтролируемый рост строк!
class RestServiceImpl extends RestServiceMock {   const RestServiceImpl();      final String name = 'Акакий';      @override   void logout() {     _getToken();     print('logout $name');   }      @override   String getProfileData() => 'getProfileData: $name';      void _getToken() => print('Here we get Token'); }

Как решать задачу

Если в интерфейсе слишком много методов, это повод задуматься, не нарушаем ли мы принципы SOLID, а именно Interface Segregation.

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

Реализовать это можно: в Swagger запросы обычно разделены на группы, такие как авторизация, профиль и т.д. На основе них можно разделить и классы.

Однако есть нюанс: при таком делении в DI (Dependency Injection) появится больше классов-зависимостей (например, ProfileApi, AuthApi) вместо одного RestService. То есть в логику (в нашей архитектуре этоInteractor‘ы) можно будет передавать отдельные интерфейсы групп запросов.

С одной стороны, это удобно — можно иметь доступ только к функциям группы. С другой, есть проблема: придется каждый раз добавлять новую группу, если нужный метод находится только в ней, хотя раньше все методы были доступны в RestService.

В Dart можно добавить к классу несколько интерфейсов, так что можно создать RestServiceImpl, который реализует все нужные интерфейсы и методы. Через DI мы будем обращаться к нему, используя только нужный интерфейс. В результате вместо одного большого RestService получим множество файлов для каждой группы и их реализаций. Получается проблема, озвученная в самом начале, решена? — Да, и это классический подход, верный по SOLID, который удобен, если, например, одна из групп (скажем, авторизация) перейдёт на другой сервис, такой как GraphQL.

Задействуем миксины

Есть и другой способ поделить крупные классы — можно использовать миксины. Этот подход заключается в том, чтобы вынести реализацию методов в mixin’ы и хранить их в отдельных файлах. Никакие дополнительные внедрения в DI не нужны, новые файлы будут состоять только из миксинов, и в архитектуре новых сущностей не появится.

mixin AuthPart on RestServiceImpl {   @override   void logout() {     _getToken();     print('logout $name');   } }  mixin ProfilePart on RestServiceImpl {   @override   String getProfileData() => 'getProfileData: $name'; } 

Но как подключать миксины? Смотрите, в Dart есть интересная возможность 👇

Перенаправление конструкторов
// Создаём приватный сервисный класс наследник от реализации (RestServiceImpl) // Который использует все наши миксины с реализацией методов (AuthPart, ProfilePart) class _RestApiSplitedService extends RestServiceImpl with AuthPart, ProfilePart {    _RestApiSplitedService() : super._(); }  // Определяем у RestServiceImpl стандартный конструктор как factory, в котором подменяем его на _RestApiSplitedService. class RestServiceImpl extends RestServiceMock {   const RestServiceImpl._();      factory RestServiceImpl() = _RestApiSplitedService;      final String name = 'Акакий';      void _getToken() => print('Here we get Token'); } 

Таким образом, нам будет казаться, что мы используем RestServiceImpl, а при вызове runtimeType окажется, что это _RestApiSplitedService.

Полный код примера
void main() async {   final RestServiceImpl restService = RestServiceImpl();     print(restService.getProfileData());     restService.logout(); }  abstract interface class RestService{   const RestService();      String getProfileData();   void logout(); }  class RestServiceMock implements RestService{   const RestServiceMock();      @override   void logout() => print('logout mock');      @override   String getProfileData() => 'getProfileData mock'; }  class RestServiceImpl extends RestServiceMock {   const RestServiceImpl._();      factory RestServiceImpl() = _RestApiSplitedService;      final String name = 'Акакий';      void _getToken() => print('Here we get Token'); }  class _RestApiSplitedService extends RestServiceImpl with AuthPart, ProfilePart {    _RestApiSplitedService() : super._(); }  mixin AuthPart on RestServiceImpl {   @override   void logout() {     _getToken();     print('logout $name');   } }  mixin ProfilePart on RestServiceImpl {   @override   String getProfileData() => 'getProfileData: $name'; } 

Можно открыть в DartPad и поиграться!

Выводы

Разделение крупных классов на несколько файлов с помощью миксинов — это быстрое решение, которое помогает уменьшить конфликты при merge и упростить отслеживание истории изменений в Git для конкретных файлов.

Кейс выше хотя и противоречит принципу SOLID, разделению интерфейсов, но, возможно, в нестандартной ситуации данный метод будет полезен, так что берите на вооружение. 


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


Комментарии

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

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