Использование примесей во Flutter приложениях

от автора

Я работаю в компании, занимающейся разработкой игр, но как домашнее хобби мне последнее время стала интересна разработка мобильных приложений. Поэтому, когда друг пригласил меня съездить на митап, посвященный разработке мобильных приложений с помощью фреймворка Flutter, я с удовольствием согласился. Попробовав там Flutter в действии, я решил обязательно изучить эту технологию. Поскольку Dart, необходимый для разработки, мне был незнаком, изучение языка также включилось в обязательную программу. Немного посидев над примерами кода, я нашел Dart простым в понимании и лаконичным языком, что мне очень понравилось. Одной из особенностей Dart, которая мне приглянулась, являются примеси.

Что такое примеси?

Для начального знакомства я приведу выдержку из Википедии.

Примесь (англ. mix in) — элемент языка программирования (обычно класс или модуль), реализующий какое-либо четко выделенное поведение. Используется для уточнения поведения других классов, не предназначен для порождения самостоятельно используемых объектов.

В языке Dart подобные конструкции определяются словом mixin перед названием.

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

Чем то напоминает возможности множественного наследования? Да, но как мне кажется, подход с примесями лучше. А почему, давайте рассмотрим на примере.

Предположим у нас есть абстрактный класс Animal.

abstract class Animal {     void voice(); } 

А также классы Cat и Dog, реализующие класс Animal.

class Cat extends Animal {     void voice() {         print(“Meow”);     } }  class Dog extends Animal {     void voice() {         print(“Woof”);     } } 

И вот нам вдруг потребовался…

catdog

Да да, лично у меня во время разработки и не такое бывает.

И в случае множественного наследования, мы бы поступили подобным образом.

class CatDog extends Cat, Dog { } 

Но как только мы нашему питомцу озвучим команду голос, получим весьма неприятную ситуацию — непонятно что именно он должен ответить, ведь метод voice реализован в обоих классах. Данная ситуация широко известна и носит название проблема ромба или Deadly Diamond of Death.

Deadly Diamond of Death

В случае реализации через примеси, мы с ней не столкнемся.

сlass Animal {    void voice() {       print(“Hakuna Matata!”);    } }  mixin Cat {    void voice() {       print(“Meow”);    } }  mixin Dog {    void voice() {       print(“Woof”);    } }  class CatDog extends Animal with Cat, Dog { } 

И что же мы услышим если теперь дадим команду голос? В данном случае — Woof, и как вы уже поняли, зависит это от порядка добавления примесей. Происходит так, потому что добавление их не параллельное, а последовательное.

Класс Animal я реализовал специально, чтобы отметить особенность появившуюся в версии Dart 2.1. До нее добавлять примеси можно было лишь к классам, наследующимся от Object. Начиная с версии 2.1 реализовано добавление к наследникам любых классов.

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

abstract class Sportsman {     void readySteadyGo(); }  mixin SkiRunner {     void run() {         print(“Ski, ski, ski”);     } }  mixin RifleShooter {     void shot() {         print(“Pew, pew, pew”);     } }  class Shooter() extends Sportsman with RifleShooter {     void readySteadyGo() {         shot();     } }  class Skier() extends Sportsman with SkiRunner {     void readySteadyGo() {         run();     } }  class Biathlete() extends Sportsman with SkiRunner, RifleShooter {     void readySteadyGo() {         run();         shot();     } } 

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

В ходе разработки вполне может возникнуть ситуация, когда функционал какой-то из примесей не должен быть общедоступен для включения всеми классами. И механизм, который позволит нам наложить данные ограничения также имеется. Это ключевое слово on в объявлении примеси вместе с названием класса. Так мы ограничим использование примеси только классами которые реализуют указанный или наследуются от него.

Например:

class A { }  abstract class B { }  mixin M1 on A { }  mixin M2 on B { } 

Тогда мы сможем объявить подобные классы:

class C extends A with M1 { } class D implements B with M2 { } 

Но получим ошибку пытаясь объявить подобное:

class E with M1, M2 { } 

Использование во Flutter приложении

Как я уже упоминал выше, примеси позволяют избавиться от дублирования кода и вынести отдельные логические части, которые можно многократно использовать. Но как это вообще применимо к Flutter, где и так все атомарно и разбито на виджеты, отвечающие за определенный функционал? Как пример мне сразу представилась ситуация в которой в проекте используется много виджетов, отображение которых меняется в зависимости от определенного внутреннего состояния. Я буду рассматривать данный пример в архитектуре BLoC и использовать сущности из библиотеки rxDart.

Нам потребуется интерфейс для закрытия контроллера потока.

/// Interface for disposable objects abstract class Disposable {     void dispose(); } 

Примесь с помощью которой мы реализуем поддержку состояний.

/// Mixin for object which support state mixin StateProvider implements Disposable {     static const NONE_STATE = "None";      final _stateController = BehaviorSubject<String>(seedValue: NONE_STATE);     Observable<String> get stateOut => _stateController.stream;      String get currentState => _stateController.value;       void setState(String state) {         _stateController.sink.add(state);     }      @override     void dispose() {         _stateController.close();     } } 

Логическая часть, которая будет управлять состоянием виджета. Пусть она по вызову метода выставит некоторое состояние и по прошествии 3 секунд сменит его на другое.

/// Example BLoC class ExampleBloc implements Disposable with StateProvider {     static const EXAMPLE_STATE_1 = "EX1";     static const EXAMPLE_STATE_2 = "EX2";      Timer _timer;      void init() {         setState(EXAMPLE_STATE_1);          _timer = new Timer(const Duration(seconds: 3), () {             _timer = null;             setState(EXAMPLE_STATE_2);         });     }      @override     void dispose() {         if (_timer != null) {             _timer.cancel();             _timer = null;         }     } } 

И сам виджет который будет реагировать на изменение состояния. Получение нужного логического компонента представим с помощью Dependency Injection.

class ExampleWidget extends StatelessWidget {     final bloc = di<HomePageBloc>();      @override     Widget build(BuildContext context) {         return StreamBuilder(             stream: bloc.stateOut,             builder: (BuildContext context, AsyncSnapshot<String> snapshot) {                 // build widget by state             },         );     } } 

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

Заключение

Механизм использования примесей мне показался довольно интересным и гибким инструментом разработки, который дает возможность построения простой, понятной и удобной архитектуры. Лично для себя я решил, что данный инструмент в моём наборе лишним явно не станет, надеюсь, что и вам он пригодится.

Ресурсы:
A tour of the Dart language
Википедия


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