Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров2K

Всем привет! Это статья для тех, кто интересуется фреймворком 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, разделению интерфейсов, но, возможно, в нестандартной ситуации данный метод будет полезен, так что берите на вооружение. 

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+3
Комментарии4

Публикации

Истории

Работа

iOS разработчик
10 вакансий
Swift разработчик
10 вакансий

Ближайшие события

25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань