Я работаю в компании, занимающейся разработкой игр, но как домашнее хобби мне последнее время стала интересна разработка мобильных приложений. Поэтому, когда друг пригласил меня съездить на митап, посвященный разработке мобильных приложений с помощью фреймворка 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”); } }
Да да, лично у меня во время разработки и не такое бывает.
И в случае множественного наследования, мы бы поступили подобным образом.
class CatDog extends Cat, Dog { }
Но как только мы нашему питомцу озвучим команду голос, получим весьма неприятную ситуацию — непонятно что именно он должен ответить, ведь метод voice реализован в обоих классах. Данная ситуация широко известна и носит название проблема ромба или 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/
Добавить комментарий