Эмулируем множественное наследование
Основания для оптимизма
Отсутствие множественного наследования - это одна из тех вещей, которая сильно раздражает программистов, переходящих с С++ на Java. (Есть еще довольно объемистый список таких вещей. Например, отсутствие перегрузки операторов и типизованных коллекций. А также отсутствие параметров по умолчанию в функциях и т.д.) К счастью, множество недостатков ActionScript первой версии слегка компенсируется тем, что какие-то вещи можно в той или иной степени эмулировать. Например, параметры по умолчанию эмулировать совсем просто:
function defArgExample(_text, _prefix){ if (_prefix != undefined) trace(_prefix + ": " + _text); else trace(_text); return; } defArgExample("building 8"); defArgExample("building 8", "address");
Этот код выводит
building 8 address: building 8
Или можно сделать даже так:
function defArgExample1(_text){ var tmpString; if (arguments.length > 1){ for (var i=arguments.length; -i>=1;) tmpString += arguments[i] + ": "; trace(tmpString + _text); } else trace(_text); return; } defArgExample1("building 9"); defArgExample1("building 9", "address"); defArgExample1("building 9", "address", "official data");
в результате чего получим:
building 9 address: building 9 official data: address: building 9
Вот и все - параметры по умолчанию готовы. Ранее вы могли видеть, как были эмулированы статические и приватные поля и методы. Так что возможности для эмуляции "всего на свете" интерпретатор ActionScript предоставляет весьма богатые. А это, в свою очередь, дает нам некоторые основания для оптимизма и в задаче об эмуляции множественного наследования.
Переходим к делу
Сразу отметим: авторы осознают, что проекты такого размера, которые требуют полноценного множественного наследования, на Флэш МХ вряд ли будут делаться. Хотя: как знать? Так или иначе, данная лекция является, скорее, демонстрацией возможностей языка ActionScript (а также и ECMA-Script, который, напоминаем, в виде JavaScript используется в браузерах). И хотя полноценное множественное наследование вам, скорее всего, не понадобится, возможно некоторые приемы, которыми мы сейчас воспользуемся, когда-нибудь используете и вы.
Теперь поставим задачу более строго. Конечно, не составит никакого труда просто скопировать все методы и поля из одного класса в другой с помощью кода наподобие следующего.
for (var name in sourceObj){ destObj[name] = sourceObj[name]; }
При этом надо взять в качестве sourceObj не что иное, как sourceClass.prototype; аналогично, в качестве destObj берем destClass.prototype. Более того, видимо, именно так и следует поступить, если все, что вам нужно от одного из базовых классов - это его поля и методы. Совсем другая ситуация возникает, если серьезная инициализационная работа производится в конструкторах, которые в свой черед вызывают базовые конструкторы и т.д. В таком случае цепочку __proto__ "сворачивать", копируя все методы, нельзя. Но возникает другая идея: а нельзя ли скопировать эту цепочку целиком? Скопировать каждую из цепочек (для каждого из базовых классов), а затем составить эти "поезда" друг за другом!
Идея хорошая, но давайте посмотрим, какие трудности нам придется преодолеть в процессе ее реализации.
Во-первых, надо ли копировать цепочки? Нельзя ли просто установить ссылку __proto__ самого последнего из базовых классов (для определенности будем дальше говорить "самого верхнего класса") одной из цепочек так, чтобы она указывала на самый нижний класс в другой цепочке? К сожалению, это невозможно, ибо приведет к "порче" ряда классов из первой цепочки - у них вдруг появятся базовые, которых отродясь не бывало. А ведь эти классы запросто могут использоваться безо всякого отношения к нашему множественному наследованию. Конечно, лишние базовые классы - это не то же самое, что недостающие; поведение изменится лишь в небольшом числе специальных случаев; но все равно неприятно. Так что копировать нужно.
Во-вторых, самый первый из конструкторов каждой субцепочки (так мы будем называть всю цепочку одного из базовых классов нашего "множественного наследника") надо будет вызывать по отдельности. И специальным образом формировать для каждого из таких конструкторов массив аргументов. Впрочем, здесь нет ничего необычного, ведь примерно то же самое происходит при вызове базовых конструкторов в С++.
В-третьих, как только мы начинаем работать с множественным наследованием, немедленно возникает проблема дублирования базовых классов. Что делать, если один и тот же класс повторяется в двух различных субцепочках? Один из естественных ответов на этот вопрос - не делать ничего. Поскольку у нас в субцепочках будет все-таки не сам класс, а его копии, такая ситуация ничем особенным не грозит. Однако есть два неудобства: а) занимается лишнее место в памяти и б) конструкторы этого класса в разных субцепочках могут делать вещи, которые конфликтуют друг с другом. В С++ обе эти проблемы решаются введением виртуальных базовых классов. Нам нужно будет предусмотреть механизм, который позволит иметь вместо двух совпадающих классов в разных субцепочках один класс, причем его конструктор будет вызываться прямо из конструктора "множественного наследника", как это делается для виртуальных базовых классов в С++.
В-четвертых, нужно уметь как следует работать с системными базовыми классами. Скажем, к последней субцепочке можно прикрепить сам системный класс, а не его копию (поскольку к нему в "хвост" уже ничего крепиться не будет). Наконец, если в разных субцепочках - разные системные базовые классы, надо уметь правильно их скопировать, включая скрытые поля и методы. Для этого, конечно, придется на короткое время все скрытое "приоткрыть", однако потом нужно будет аккуратно спрятать все обратно.