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