«Настоящие» примеси на классах JavaScript

«Настоящие» примеси на классах JavaScript

Примеси и JavaScript как классическое кино с Клинтом Иствудом.

Они хороши тем, что позволяют создать объект из настолько маленьких кусков, насколько это позволяет гибкая природа JavaScript, поэтому примеси довольно популярны в определенных кругах.

Список плохих качеств немного больше:

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

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

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

Для тех, кому нравятся примеси, и думают, что они должны использоваться везде, где возможно - это ужасно. Примеси решают множество проблем, которые есть у языков с одиночным наследованием, и, как по мне, большинство жалуются на переполненность прототипов, в сравнении с классами в JavaScript. Я бы сказал, что все наследование должно иметь интерфейс примесей: подклассы - это просто вырожденная форма применения примесей.

К счастью, есть свет в конце тоннеля, с JavaScript классами.

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

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

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

Перед тем, как разобраться в том, что должна делать реализация примесей, давайте сначала рассмотрим, чем являются примеси:

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

Gilad Bracha and William Cook, Mixin-based Inheritance

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

Для копания в глубину этого определения, давайте добавим два новых термина в наш словарь:

  • Определение примеси: Определение класса который может быть применен к разным суперклассам.
  • Применение примеси: Применение определения примеси к суперклассу порождает новый подкласс.

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

Единственное отличие между примесью и обычным подклассом в том, что класс имеет фиксированный суперкласс, в то же время примесь на момент определения его не имеет.

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

Примеры

Вот пример примесей на Dart, который имеет красивый синтаксис, похожий на JavaScript:

Где A - базовый класс, B - подкласс, и M - примесь. Применение примеси - это специфическая комбинация M добавленная в A , часто называемая A-with-M . Суперкласом A-with-M является A , и реальным суперклассом B является не A , как вы могли ожидать, а A-with-M .

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

Начнем с простой иерархии классов, где класс B наследуется от класса A :

Теперь добавим примесь:

Как можно заметить, применение примеси A-with-M вставлено в иерархию между подклассом и суперклассом.

Примечание: Я использовал линию с длинными пунктирами для представления объявления примеси ( B включает М ), и линию с короткими пунктирами для представления применения определения примеси.

Множественные примеси

В Dart множественные примеси применяются в порядке слева направо, результирующие множественные добавляются в иерархию наследования:

Классические примеси JavaScript

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

Например: библиотеки примесей, Cocktail, traits.js, и описанные шаблоны, которые можно найти в многих статьях (как эту в последнем Hacker News: Использование декораторов ES7 как примеси), в основном работают модифицируя прототип, копируя свойства из объектов примесей и переписывая существующие свойства.

Очень часто реализованы функцией, похожей на эту:

Ее вариант даже существует в JavaScript, известный как Object.assign.

mixin() обычно вызывается на прототипе:

в результате чего, MyClass имеет все свойства определенные в MyMixin .

Чем же это плохо?

Простое копирование свойств в целевой объект имеет несколько проблем. Некоторые из этих проблем можно обойти используя "умные" функции примесей, но все же большинство имеют эти проблемы:

Модификация прототипа

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

Не работает super

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

Неправильный приоритет

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

Нарушенная композиция

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

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

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

Лучшие примеси с классами

Теперь давайте перейдем к хорошим штукам: Потрясающие примеси™ 2015 Edition .

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

  1. Примеси добавлены в прототип
  2. Примеси применяются без изменения существующих
  3. Примеси не имеют магии, и не определят новую семантику поверх ядра
  4. доступ к свойству super.foo работает и
  5. вызовы super() работают
  6. Примеси могут расширять другие
  7. работает instanceof
  8. Определение примесей не требует библиотек для своей работы, они могут быть написаны в универсальном стиле.
Фабрики подклассов со странным трюком

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

Мы полагаемся на две особенности JavaScript

  1. class может быть использован как выражение так и оператор. Выражение возвращает новый класс каждый раз при вызове (подобие фабрики!).
  2. extends принимает произвольное выражение, которое возвращает класс или конструктор.

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

Все, что нам надо определить примесь как функцию, которая получает суперкласс и создает новый подкласс из него, например так:

После, мы можем использовать его вот так:

Теперь MyClass имеет метод foo с помощью наследования примеси:

Невероятно простой и невероятно мощный! Просто комбинируя применение функции и выражения класса, мы получили целое решения для примесей, которые вполне обобщенные.

Применение нескольких примесей так же работает:

Еще примеси могут легко наследоваться от других примесей, просто передавая суперкласс:

И вы можете использовать обычную композицию для композиции примесей:

Польза примесей как фабрики подклассов

Этот подход дает нам очень хорошую реализацию миксин.

Подклассы могут переопределить методы примеси

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

Работает super

Одно из наибольших преимуществ это то, что super работает внутри методов подклассов и примесей. Так как мы никогда не затираем методы классов или примесей, то они всегда доступны для адресации через super .

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

Сохраняется композиция

На самом деле это только следствие других преимуществ, но две примеси могут определить одинаковые методы, и до тех пор, пока они вызывают super , оба метода будут вызваны и применены.

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

Улучшение синтаксиса

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

Я бы хотел, чтобы синтаксис был проще для глаз и как минимум дал новым читателям возможность понять, что происходит, к примеру, как синтаксис в Dart. Так же, я бы хотел добавить дополнительные функции, например, меморизацию применения примеси и автоматическую реализацию поддержки instanceof .

Для этого я написал простой помощник, который применяет список примесей к суперклассу:

Вот его реализация:

Конструкторы и инициализация

Конструктор - источник для потенциальной путаницы с примесями. Они специально ведут себя как методы, за исключением того, что переопределенные методы склонны иметь одинаковую сигнатуру, в то время как конструкторы и иерархии наследования часто имеют разные сигнатуры.

Так как примеси не знают к какому классу будут применены, и как в следствие сигнатуру супер-конструктора, вызов super() может быть довольно мудреный. Поэтому, лучше всего это всегда передавать все параметры конструктора в super() , или не определять конструктор вообще, или использовать оператор расширения: super(. arguments) .

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

📎📎📎📎📎📎📎📎📎📎