«Настоящие» примеси на классах 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 .
Давайте быстро пересчитаем список функций, которые мы б хотели иметь, и по ним сможем оценить нашу реализацию.:
- Примеси добавлены в прототип
- Примеси применяются без изменения существующих
- Примеси не имеют магии, и не определят новую семантику поверх ядра
- доступ к свойству super.foo работает и
- вызовы super() работают
- Примеси могут расширять другие
- работает instanceof
- Определение примесей не требует библиотек для своей работы, они могут быть написаны в универсальном стиле.
Выше я ссылался на примеси как "подкласс фабрик, параметризованные суперклассом", и это в буквальном смысле.
Мы полагаемся на две особенности JavaScript
- class может быть использован как выражение так и оператор. Выражение возвращает новый класс каждый раз при вызове (подобие фабрики!).
- extends принимает произвольное выражение, которое возвращает класс или конструктор.
Ключевым значением есть то, что классы в JavaScript являются "полноправными гражданами": являются значениями и могут быть переданы и возвращенными из функций.
Все, что нам надо определить примесь как функцию, которая получает суперкласс и создает новый подкласс из него, например так:
После, мы можем использовать его вот так:
Теперь MyClass имеет метод foo с помощью наследования примеси:
Невероятно простой и невероятно мощный! Просто комбинируя применение функции и выражения класса, мы получили целое решения для примесей, которые вполне обобщенные.
Применение нескольких примесей так же работает:
Еще примеси могут легко наследоваться от других примесей, просто передавая суперкласс:
И вы можете использовать обычную композицию для композиции примесей:
Польза примесей как фабрики подклассов
Этот подход дает нам очень хорошую реализацию миксин.
Подклассы могут переопределить методы примесиКак я говорил ранее, многие примеры примесей неправильно понимают и примеси переписывают методы подкласса. С этим подходом подклассы правильно переопределяют методы примесей, которые в свою очередь переопределяют методы суперкласса.
Работает superОдно из наибольших преимуществ это то, что super работает внутри методов подклассов и примесей. Так как мы никогда не затираем методы классов или примесей, то они всегда доступны для адресации через super .
Вызовы super могут быть немного не очевидными для новичков в примесях, потому что суперкласс не известен на момент определения примеси, и иногда разработчики ожидают, что super указывает на определенный суперкласс (параметр на миксин), а не применение примеси. Может помочь мысль об конечной цепочке прототипов.
Сохраняется композицияНа самом деле это только следствие других преимуществ, но две примеси могут определить одинаковые методы, и до тех пор, пока они вызывают super , оба метода будут вызваны и применены.
Иногда примесь может не знать, существует ли в суперкласса необходимое свойство или метод, поэтому лучше для начала убедиться в этом:
Улучшение синтаксиса
Я нашел примеси как функции одновременно элегантно простыми (можно понять, что собственно происходит), и в то же время немного уродливыми. Меня больше всего беспокоит то, что конструкция не оптимальная для читателей, которые не знакомы с этим подходом.
Я бы хотел, чтобы синтаксис был проще для глаз и как минимум дал новым читателям возможность понять, что происходит, к примеру, как синтаксис в Dart. Так же, я бы хотел добавить дополнительные функции, например, меморизацию применения примеси и автоматическую реализацию поддержки instanceof .
Для этого я написал простой помощник, который применяет список примесей к суперклассу:
Вот его реализация:
Конструкторы и инициализация
Конструктор - источник для потенциальной путаницы с примесями. Они специально ведут себя как методы, за исключением того, что переопределенные методы склонны иметь одинаковую сигнатуру, в то время как конструкторы и иерархии наследования часто имеют разные сигнатуры.
Так как примеси не знают к какому классу будут применены, и как в следствие сигнатуру супер-конструктора, вызов super() может быть довольно мудреный. Поэтому, лучше всего это всегда передавать все параметры конструктора в super() , или не определять конструктор вообще, или использовать оператор расширения: super(. arguments) .
Это значит, что передача аргументов особенно для конструкторов примесей - сложно. Один простой обходный путь - это просто иметь явный метод инициализации примеси, если он требует дополнительных аргументов.