Всем привет! Это статья для тех, кто интересуется фреймворком 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/
Добавить комментарий