Я работаю в компании, занимающейся разработкой игр, но как домашнее хобби мне последнее время стала интересна разработка мобильных приложений. Поэтому, когда друг пригласил меня съездить на митап, посвященный разработке мобильных приложений с помощью фреймворка 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
Википедия