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

    Несколько раз от коллег звучал вопрос о том, что не понятно, зачем вообще нужны 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 {}

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


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


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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 23

      +1

      В статье мало наркомании)


      А так было интересно.

        0
        Я правильно понимаю что миксин нельзя ограничить типами на которые он может применяться и из него никак не обратиться к методам и полям типа к которому он применен?
          0

          Можно и то, и другое. Например, вот так:


          abstract class Animal {
            void run() {}
          }
          
          mixin SpeedRunner on Animal {
            void speedRun() {
              run();
              run();
            }
          }
          
          class Ostrich extends Animal with SpeedRunner {}
          
          // Ostrich().speedRun();
            0
            О, спасибо, выглядит действительно довольно удобно, как прокачанные экстеншены из того же котлина.
              0

              Только в котлине или расте для этого не надо создавать новый тип как в данном случае. Можно имплементировать extension или trait сразу для любого типа, например, числового и вызвать метод: 1.foo(). В целом миксины похожи на них, но с уклоном в обычное множественно наследование и не такие могущественные.

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

                  Ну наверное это ближе к растовым трейтам и хаскелевым тайпклассам. Но только в них нельзя хранить данные. Хотя имеет ли это большой смысл на самом деле? В таком случае можно просто разрешить множественное наследование классов в языке и не париться.

                    0

                    Множественное наследование тянет за собой diamond problem, так что от него уже все шугаются. Плюс не всегда нужна семантика is-a, миксины хорошо ложатся на текущий курс партии — composition over inheritance.


                    Когда полезно иметь данные в миксине — например в геймдеве. Если хочется чтобы объект имел свойства 'Serializable', 'Drawable', 'InventoryOwning' и 'Killable' (и ещё много чего), то реализовывать через множественное наследование — разлапистая иерархия получается, и изменять потом сложно. Легче набрать объект из миксинов, тем более что тесное взаимодействие с основным классом будет наоборот вредить гибкости.

                      0

                      Вышеперечисленные свойства похожи на обычные интерфейсы.

                        0

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

                  +1

                  В Dart есть экстеншены:


                  extension SuperInt on int {
                    List<int> to(int end) => List.generate(end - this + 1, (i) => this + i);
                  }

                  После этого можно делать так:


                  final range = 2.to(5); // range = [2, 3, 4, 5]
              0
              Можно обращаться, если явным образом указать через mixin A on B, где B — родительский класс.

              gist.github.com/kevmoo/b60dc2fc7ea49acecb1fd2b57bf9be57
                0
                Спасибо, действительно удобно.
              0
              Серьезно? Вы пол статьи тратите на размышления по поводу логичности абстрактного примера из чужой статьи? На примеры стоит смотреть через призму того что они пытаются продемонстрировать, а не того, как можно было бы сделать это иначе. У примера с животными была конкретная цель — показать выгоду по сравнению с DDD. У примера с спортсменами, как раз то к чему вы пытались прийти. Для вынесения общего функционала, который может потребоваться в разных местах. В том числе и в ситуациях когда нет общего наследника, так и в ситуациях когда он есть. Как в ситуациях, когда это важное свойство, так и в ситуациях когда оно незначительно. Так что вывод «зачем вообще нужны mixin'ы» можно сделать как из моей статьи, так и из кучи англоязычных в сети, странно что у вас это не получилось.
              К слову, вы пытались привести пример из флаттера, так вот в самом движке есть намного более яркий пример SingleTickerProviderStateMixin. Он как раз выносит важный функционал для воспроизведения анимаций в отдельный миксин, хотя используется этот миксин классамм с общим родителем, там даже ограничение стоит — «on State». Так что, по-моему скромному мнению, чтобы восполнить пробел существующих статей, нужно было сначала изучить их поподробнее, ну и естественно посмотреть реальные кейсы, которыми сам Flutter более чем богат.
                0

                Прошу прощения, если я Вас обидел

                  0
                  Вы меня совершенно не обидели. Вы не правильно поняли мой комментарий, судя по всему. Хотелось хотя бы более глубокого погружения в тему, раз уж вы решили восполнить пробелы информации в данной теме (хотя с этим я совершенно не согласен, информации более чем достаточно). А что на выходе — вы разбираете чужой пример выдернутый из контекста, потом приводите свой пример на основе которого делаете совсем не полный вывод, я бы даже сказал ошибочный, подавая его как правильный юзкейс. Самое правильное что вы написали, так это последний абзац. Вариантов использования в различных ситуациях можно придумывать до бесконечности. А учитывая, что вы хотели дать ответ на вопрос, когда же лучше использовать данный механизм, гораздо полезнее было бы уж тогда хотя бы рассмотреть кучу разных кейсов, при том рассмотреть их подробно. А в итоге статья получается не выполняет даже ту цель, которую вы поставили перед ней сами вначале. Удачи вам в последующих статьях, надеюсь вы здраво воспримете мою критику.
                    0

                    Моя цель была не в том, чтобы разобрать тысячи вариантов использования примесей. Серди этих тысяч есть и хорошие и плохие. Моя задача была показать, в каких случаях использование примесей дает преимущество перед наследованием и интерфейсами. А если мы говорим о критике, то я рад критике, но критика бывает разной: конструктивной и деструктивной. При конструктивной критике, когда говорят, что есть ошибка, то показывают, в чем она. При деструктивной — просто говорят, что все не правильно без всяких пояснений. Вы сказали, что описанные мной варианты применения примесей ошибочны, но не показали, в чем именно эта ошибка. И тон Вашего обращения агрессивен, потому и был сделан вывод, что Вы обиделись на то, что Ваш пример был подвергнут критике (замечу, что конструктивной, так как было указано, что в этом примере не так). Если у Вас есть конструктивные замечания, то я готов их услышать. Если же их нет, то лучше закончить эту дискуссию.

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


                      В итоге вы приводите всего 1 пример и делаете вывод:

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


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

                      Киллер и биатлонист, оба стреляют у обоих это важное свойство, невозможно юзать миксин?

                      Слишком абстрактно?

                      Я привел пример, который вы судя по всему даже не открыли, раз пишите что я не представляю вам конструктивной критики.
                      Вы указали как тег своей статьи Флаттер в том числе. Наверное реализации в самом Флаттере тоже могут быть аргументом в таком случае.
                      Я привел вам пример SingleTickerProviderStateMixin это довольно важная часть функционала по реализации анимаций, и это миксин можно прицепить к различным стейтам.
                      Анимация для визуального класса не значимая вещь?

                      И да, я не предлагаю все подряд выносить в миксины, это то еще зло в красивой обертке. Любым инструментом надо пользоваться аккуратно и к месту, можно и фугасом попытаться гвоздь забить, может получиться. А может и нет.

                      Следующий момент, касаемо целей — вы поставили цель восполнить пробел, ну так показали бы разные кейсы, а то выходит кейс 1 со спорным выводом. Так и позиционировали бы статью как один из кейсов который мне кажется правильным.

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

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


                        abstract class Shooter {
                          void shoot();
                        }
                        
                        class Killer extends Shooter {}
                        
                        class Biathlonist extends Shooter {}

                        И именно то, что ни киллер, ни биатлонист без стрельбы не могут существовать, говорит о том, что использование примесей в данном случае не является обоснованным. Примесь хороша тогда, когда объект остается самим собой, независимо от того, есть свойство, реализованное в примеси или нет.


                        Также не нужно путать важное свойство и существенное свойство. Свойство может быть важным, но не существенным. В частности в приведенном Вами примере с SingleTickerProviderStateMixin. Этот mixin безусловно добавляет важное свойство к виджету, но не существенное, так как при наличии или отсутствии этого свойства виджет остается виджетом. А вот если у виджета убрать или добавить существенное свойство, то он либо перестанет быть виджетом вообще, либо станет принципиально другим виджетом (примером принципиально разных виджетов являются StatelessWidget и StatefulWidget)


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

                          0
                          Только вот незадача, киллер помимо того что Shooter он еще и Bandit и немного болтает на фене, как и Thief который от того же Bandit наследуется, но предвкушая наследование бандита от стрелка, Thief очень искусный щипач и с его слов «в гробу он эту стрельбу видел». А биатлонист, который Shooter и еще спортсмен, который по дефолту имеет метод тренироваться и хранит медальки. А еще у нас есть стрелок стендовик, который тоже спортсмен и тоже стреляет, но увы без лыж, говорит не удобно ему в них стрелять. И бухгалтер Оля, которая в свободное время любит ходить в тир, и жизни своей без того чтоб засадить фигурке медведя куда-нибудь не представляет. И да, конечно же у нас есть интерфейсы, я не оспариваю что интерфейсы это хорошо, это очень удобно и сам их юзаю. Пусть в жизни они все стреляют по разному, кто-то человечков валит, кто-то в мишени палит, а кто-то по тарелочкам садит. Но вот что если на уровне абстракции который выбран нами, вся эта стрельба имеет абсолютно одинаковую реализацию. Мы получаем дубль абсолютно одинакового кода, который мы пишем реализуя интерфейсы. Так что функционал существенный, но для всех один и тот же. Кстати использование примеси не исключает использование интерфейсов, это к слову.

                          А касаемо примера из Флаттера, ну вы бы хотя бы открыли что ли его для приличия. Ну право, он ограничение на State имеет, ну причем здесь виджеты. И юзается в куче его наследников, в том числе те которые напрямую относятся к стейтам для которых анимация не важное а существенное свойство, например ImplicitlyAnimatedWidgetState. Это даже из названия следует. И в таких для которых это не является существенным свойством, например _HelperErrorState. Видимо команде флаттера надо как можно скорее переписывать эту часть, ведь она не покрывается вашим случаем на все случаи жизни.
                            0

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


                            ну причем здесь виджеты

                            Виджеты здесь при том, что речь идет о состоянии виджета.


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

                              0
                              Куда уж дальше от реальной разработки, чем тот же пример из флаттера. Согласен, лучше закончить эту дискуссию, видимо Вы из тех зашоренных людей, у которых все вокруг «неправы, один он Д'Артаньян».
                              Но с Вашей стороны я не вижу потребности это понять

                              Зря Вы считаете, что я не понимаю то что вы написали, у меня тоже достаточно опыта реальной разработки. И я не пытаюсь доказать что пример ваш не правилен, я пытался Вам показать, что возведение в абсолют чего-либо это неправильная позиция, и узкость. Но увы, видимо Вы не имеете нужды это увидеть, Вам в своём мирке уютно, и ничего кроме него Вы не хотите видеть. Это Ваше право. На этом всё, в одном мы сошлись — каждый при своём.
                0
                Для интереса вот тут сделал mixin с анимацией habr.com/ru/post/485002

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

                Самое читаемое