Как стать автором
Обновить

Использование примесей (mixins) в Dart

Dart *Flutter *

Несколько раз от коллег звучал вопрос о том, что не понятно, зачем вообще нужны mixin'ы (примеси) в языке Dart. Я решил посмотреть, что есть на просторах Интернета по этому вопросу. К великому огорчению в статьях, которые удалось найти в основном говорится о том, как использовать примеси, но не дается разъяснений зачем они нужны, в каких случаях их использование более предпочтительно, чем обычное наследование или реализация интерфейсов. Эта статья является попыткой восполнить этот пробел.


Несмотря на то, что в Интернете есть достаточно статей по теме примесей в Dart и Flutter, ясности они не вносят на мой взгляд потому, что приводимые примеры показывают чистую механику конструирования классов с примесями, которая далека от разумности и поэтому не демонстрирует реальную область их применения. В частности я встретил такой пример. Имеем:


class Animal {}
class Dog {}
class Cat {}

И почему-то нам захотелось получить животное, которое имеет свойства кошек и собак одновременно. В этом случае мы можем сделать так:


class CatDog extends Animal with Cat, Dog {}

К данному примеру есть по крайней мере два вопроса:


  • зачем нам может понадобиться помесь кошки с собакой?
  • почему кошка и собака не наследуют от Animal? Разве они не животные?

При этом, зачем же все-таки нужны примеси, остается загадкой.


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


Если посмотреть на наследование с иной точки зрения, то увидим, что Прямоугольник наследует свойства Фигуры, а Кошка наследует свойства Животного. Если не обращать внимания на логику, то чисто технически может захотеться унаследовать свойства нескольких разных сущностей. Для этого некоторые языки програмирования поддерживают множественное наследование.


Множественное наследование критикуется за ряд недостатков (см. Википедию), поэтому многие языки программирования не используют множественное наследование совсем, а используют механизм реализации интерфейсов и/или примеси. Да и, с точки зрения логики, конструкции, получающиеся в результате множественного наследования, не просто осмыслить.


Для понимания следующего материала необходимо вспомнить некоторые понятия из элементарной логики. В частности понятия существенных и несущественных свойств. Существенными свойствами объектов являются те, благодаря наличию которых он отностится к тому или иному классу объектов. Несущественными свойствами объекта являются те, наличие, отсутствие или конкретные значения которых не влияет на принадлежность обекта некоторому классу объектов. Например, форма прямоугольника является существенным свойством этой фигуры, так как если мы изменим эту форму (уберем или добавим сторону или изменим углы), то прямоугольник перестанет быть прямоугольником. Но если изменить размеры прямоугольника, то он все равно останется прямоугольником. Поэтому размеры — это несущественное свойство.


Выстраивание иерархии классов обычно основывается на добавлении к классу родителя каких-либо существенных свойств. Например,


abstract class Shape {
  void draw();
}

class Rectangle extends Shape {
  @override
  void draw() {
    print('Draw rectangle');
  }
}

class Circle extends Shape {
  @override
  void draw() {
    print('Draw circle');
  }
}

В основе этой иерархии лежит существенное свойство форма фигуры.


Еще пример:


abstract class Widget {
  void render();
}

class Container extends Widget {
  @override
  void render() {
    print('Renders container');
  }
}

class Text extends Widget {
  @override
  void render('Render text');
}

Здесь существенным свойством является назначение виджета.


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


Для этого можно, конечно, воспользоваться наследованием и реализовать сначала классы PaintableShape и PaintableWidget. Но это оказывается не удобным, так как, во-первых, нам придется дублировать реализацию функционала раскрашивания в обеих иерархиях, а, во-вторых, для каждой фигуры и виджета, которые мы хотим раскрашивать, нам придется реализовывать новые классы, например, PaintableRect и PaintableContainer.


Можно воспользоваться механизмом реализации интерфейсов. Тогда мы получим что-то типа такого:


enum Color {red, yellow, green}

abstract class Paintable {
  void paint(Color color);
  Color get color;
}

class PaintableRect extends Rectangle implements Paintable {
  Color _color;
  @override
  void paint(Color color) {_color = color;}

  @override
  Color get color => _color;
}

class PaintableContainer extends Container implements Paintable {
  Color _color;
  @override
  void paint(Color color) {_color = color;}

  @override
  Color get color => _color;
}

Как видно это тоже не самое лучшее решение, так как нам приходится дублировать один и тот же код для каждой раскаршиваемой сущности.


Но все эти проблемы решаются, если фукционал, связанный с несущественным свойством, мы вынесем в отдельную примесь (mixin):


enum Color {red, yellow, green}

mixin PaintableMixin {
  Color _color;
  void paint(Color color) {_color = color;}
  Color get color => _color;  
}

class PaintableRect extends Rectangle with PaintableMixin {
  @override
  void draw() {
    print('Draw rectangle with color $color');
  }
}

class PaintableContainer extends Container with PaintableMixin {
  @override
  void render() {
    print('Render container with color $color');
  }
}

Теперь можно этим пользоваться:


main() {
  PaintableRect()
    ..paint(Color.red)
    ..draw();
  PaintableContainer()
    ..paint(Color.yellow)
    ..render();
}

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


Предположим нам понадобилось добавлять к некоторым виджетам функционал, связанный с одним и тем же свойством. Виджеты во Flutter конструируются следующим образом:


class MyStatelessWidget extends StatelessWidget {}

или


class MyStatefulWidget extends StatefulWidget {}

Для добавления некоторого свойства через наследование придется реализовывать по крайней мере два класса:


class StatelessWidgetWithProperty extends StatelessWidget {}

class StatefulWidgetWithPropery extends StatefulWidget {}

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


При использовании примесей проблема решается:


mixin Property {}

class MyStatelessWidget extends StatelessWidget with Propery {}

class MyStatefulWidget extends StatefulWidget with Property {}

Для тех, кто знаком с паттеранами проектирования, использование примесей в некоторых случаях может заменить применение паттерна Мост.


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


Данная статья не претендует на исчерпывающее определение вариантов использования примесей. Вероятно пытливый ум разработчика сможет найти еще много красивых применений для них. Буду рад, если эти варианты использования примесей появятся в комментариях к этой статье.

Теги:
Хабы:
Всего голосов 15: ↑13 и ↓2 +11
Просмотры 7.7K
Комментарии Комментарии 23