Macromedia Flash 5. Объектно - ориентированное программирование

       

Упаковываем классы, используя __proto__


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

Итак, когда объект-экземпляр запрашивает некоторое свойство, не имея его при этом в наличии, автоматически проверяется прототип его конструктора (прототип класса, который его создал). Даже если и прототип не содержит нужных данных, история на этом не заканчивается. Этот самый прототип, являясь полноправным объектом, уж будьте уверены, проверит прототип класса, который создал его - Object. Короче говоря, он проверит Object.prototype. Трудно сразу проглотить такой кусок информации, давайте его пережуем.

Обратимся к очередной метафоре. Представьте себе, что владелец дома начинает требовать с вас плату за жилье. И у вас нет ни копейки (вполне возможно, что это и представлять не надо). Ваши действия? Скорее всего, обычный человек обратится за помощью к объекту (объектам), которые его создали - к родителям. Вы точно знаете, что собираетесь расплатиться именно деньгами, взятыми у родителей (вряд ли хозяин квартиры согласится принять в уплату самих родителей), поэтому вы и просите их порыться в закромах (в прототипе). Что они сделают, окажись, как и вы, в затруднительном положении? Очень может быть, что, после коротких переговоров, посоветуют вам обратиться к объектам, которые создали их - к вашим бабушкам и дедушкам. Забыв на время о том, что ничто не вечно (а также об инфляции), мы сможем представить себе картину, где вы от бабушек-дедушек идете к прабабушкам-прадедушкам, от них - к пра-пра-бабушкам-дедушкам и т.д. И так будет продолжаться до тех пор, пока вы либо не найдете нужную сумму, либо не доберетесь до Создателя Всего Сущего. В зависимости от того, кто хранитель последней надежды в системе ваших убеждений, это может быть либо Бог, либо правительство, либо, в конце-концов, Object.prototype. Что если случится так, что вы обспросили всех, молились ночи напролет, но - "Ом мани падме хум" - "Денег нет и не будет"? Что ж, у вас появится реальный шанс остаться без крыши над головой. Если же хоть кто-то даст вам нужную сумму, вы вновь обретете счастье и уверенность в себе и прекратите дальнейшие поиски. В ActionScript-е точно так же, как только затребованное свойство получено, оно немедленно передается местному хозяину и работа заканчивается. Полезно думать, пока не привыкнешь, о цепочке прототипов таким образом: куда-мне-обратиться-если-вы-тоже-сидите-без-денег. По крайней мере, это научит вас совершать определенные действия к концу каждого месяца.


Откуда объекты узнают, к кому обращаться в затруднительном положении? Им об этом рассказывают после того, как они будут созданы. Кто? Конечно, их родители. Что они при этом говорят? Они говорят: "Милый ребенок, запомни хорошенько, что если когда-нибудь в этом холодном и бесприютном мире ты будешь испытывать нужду, знай, что ты не одинок. В первую очередь обращайся к нам и мы постараемся что-нибудь придумать для тебя". Итак, пока мы еще в светлой памяти, давайте хорошенько усвоим, что классы хранят свои свойства в своих прототипах, а это значит, что объекты, не имея некоторых свойств, проверяют их наличие в прототипе класса, которым были созданы. Уф!



Здесь есть одна тонкость: прототип есть объект (именно поэтому вы могли раньше слышать словосочетание "объект прототип"). Что это значит? Это значит, что если прототип не имеет нужного свойства, он проверяет прототип класса, который его создал. И последний вопрос: как класс создает прототипы? Вы, возможно, уже знаете ответ - вспомните, что прототипы создаются автоматически, когда определяются функции и их тип "объект". Ладно, может вы не в курсе, но в самом деле прототипы создаются классом Object. Точно так же и экземпляры классов (ведь они тоже объекты) слышат в момент своего рождения от родителей: "Если что, посмотри в моем прототипе сынок". Вот так все прототипы и экземпляры в конечном итоге добираются до Object.prototype, если нигде не найдут нужного свойства. Это и есть третий лист стеклянного пакета - Object.prototype.

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

  - Слушай, Боб, как ты думаешь, ведь никто из нас не наложит в штаны,

    если мы тот класс назовем просто "Object"?



  - Ясное дело, Пит, о чем разговор!

Вот так его и окрестили Object-ом, с большой буквы "О" ( ведь имя класса всегда начинается с заглавной буквы). Существует множество типов объектов, некоторые из них являются экземплярами Object, другие лишь производные от него, но так или иначе, все они являются его наследниками. Хоть Боб с Питом давно канули в лету, дело их живет. Если вы хотите, вы можете перекрестить Object хоть в Верховного Пса (TopDog) - попробуйте и вы испытаете непередаваемое ощущение. Эх, где ж я был в тот момент...

  Не все то Объект...

Вы можете подумать, что числа и строки объекты более общего вида, чем объекты, непосредственно порожденные классом Object. А давайте посмотрим на методы, им доступные. Разве вы не встречали что-нибудь вроде myString.substring(2,3) или myString.indexOf("test")? Даже числовые объекты могут похвастаться свойствами, например, Number.MAX_VALUE, так же как toString и valueOf. Что же касается старых добрых 5 или 7 - не это ли объекты более общего вида? Снова нет, потому что это не объекты, а литералы. Точно так же и "Hello" и "World". Мы часто слышим, что в ActionScript все есть объект, но это не так. (а функции, активизирующие объект - ведь тоже не объекты, а лишь описания). Числа до тех пор не будут объектом, пока не создадутся с помощью функции Number( ).



trace( Number(5).toString() ); // 5 trace( 5.toString() ); // error

Со строками то же самое, у них есть метод String(), но они более темные лошадки. Обычные строки автоматически преобразуются в объекты, если они вызывают строковые методы (исключая методы "только для строк" - concat, fromCharCode, slice и substr). В этом случае создается временный объект, метод запускается, затем объект удаляется.

trace( "hello".substring(1,4) ); // ell

Это делает строки более гибкими в использовании, а знание этого объясняет некоторую тормознутость строковых методов подобных "split" - все должно быть преобразовано в строковые объекты, даже если это уже объект!



Для осмысления того, что все вещи произошли от Object.prototype, нужно согласиться с тем, что Object всего-навсего класс, особым его делает лишь высокое положение. Когда вы создаете объект, говоря:

x = new Object();

вы просто-напросто создаете объект самого общего вида (generic) в ActionScript (см. примечание выше). Класс Object имеет два встроенных метода в своем прототипе: toString и valueOf. Есть также еще одно свойство, которое мы рассмотрим позже. Все объекты тоже наследуют эти три свойства. Даже если объект порожден другим классом, в каком-то колене он все-равно является потомком Object. Экземпляр проверяет прототип своего класса, тот, в свою очередь, Object.prototype.

Когда вы создаете, например, массив, он будет являться объектом класса Array. У класса Array есть прототип, который обращается к Object.prototype за отсутствующими свойствами. То же самое происходит и с классом Color, Date и любым другим. Методы и свойства, содержащиеся в них, делают их уникальными, однако они могут спокойно делать то, что под силу вышестоящим объектам в их иерархии. Все от того, что, например, объект типа Color привязан не только к Color.prototype, но и к Object.prototype, потому что Color.prototype сам привязан к Object.prototype. Та же ситуация возникнет и с вашими собственными классами:

Object.prototype
Dog.prototype
rover 
У Object.prototype есть маленькая табличка с надписью: "Дальше дороги нет!".

Ну ладно, с этим мы разобрались. Давайте рассмотрим сам механизм того, откуда объект узнает о направлении поисков. Эти связи совершаются при помощи __proto__. Это и есть то самое третье свойство Object.prototype и поэтому его можно найти во всех остальных объектах. Задача его проста и сводится к тому, чтобы указывать на следующего вероятного благодетеля. Проще говоря, ему можно дать имя "Спроси-вон-того-если-тебе-что-то-нужно". Итак, имя_объекта.__proto__ указывает на Класс.prototype, этот Класс.prototype тоже объект, обладающий свойством Класс.prototype.__proto__ указывающим на Obejct.prototype. Узнать его вы всегда сможете по четырем знакам подчеркивания.





Открою вам один секрет: __proto__ МОЖНО ИЗМЕНЯТЬ. Вы вольны направить его в любую сторону. Вы можете заставить __proto__ объекта rover указывать на Cat.prototype вместо Dog.prototype и этот разбойник (rover) будет только рад поискать необходимые свойства и методы в новом месте. Причем, таким образом вы можете манипулировать не только подобными объектами. Можно попросить прототип класса порыться в месте, отличном от Object.prototype. Хитрость в том, что нужно сделать два класса и направить __proto__ прототипа первого на прототип другого (вместо Object.prototype). Таким образом вы получите "младший" класс, подкласс (SubClass) и "старший" класс, суперкласс (SuperClass). Делается это простой операцией:

SubClass.prototype.__proto__ = SuperClass.prototype;



SubClass и SuperClass просто имена; так сказать, "дедушку" можно обозвать SuperSuperClass. Если вы создадите новый объект на основе младшего класса SubClass, цепочка будет выглядеть следующим образом:

Object.prototype
SuperClass.prototype
SubClass.prototype
instance 
Запомните, мы связываем не сами классы, но их прототипы. Классы лишь описывают объекты, прототипы же содержат все необходимые свойства.

Еще одна вещь, которую необходимо знать: __proto__ можно использовать несколько раз при подъеме по иерархической лестнице. Так instance.__proto__.__proto__ указывает на SuperClass.prototype. Пусть вас не пугает этот, на первый взгляд, сложный синтаксис - просто посчитайте на рисунке красные стрелочки. Папа, дедушка, прадедушка, прапрадедушка...

Вау, пора привести пример! Давайте начнем с нескольких не связанных между собой классов, а сделать это нужно (зачем - поймем далее). Допустим у нас есть Собака (Dog), Кот (Cat) и Хомяк (Hamster):



// Dog Class Dog = function( name )
{
this.name = name;
}
Dog.prototype.legs = 4;
Dog.prototype.price = 10; Dog.prototype.pet = true;

// Cat Class Cat = function( name ) { this.name = name; } Cat.prototype.legs = 4; Cat.prototype.price = 5; Cat.prototype.pet = true;



// Hamster Class Hamster = function( name ) { this.name = name; } Hamster.prototype.legs = 4; Hamster.prototype.price = 15; Hamster.prototype.pet = true;

Как видите, здесь присутствуют нежелательные повторения, делающие программу менее гибкой. Что будет, если к оговоренным имени (name), количеству лап (legs), цены (price) и отношению хозяев к своей зверюшке (pet) мы захотим добавить новую категорию, возраст, скажем? Нам бы пришлось забивать его отдельно для каждого зверька, а это неправильно. Мало того, что возрастает объем программы, это в дальнейшем чревато всякими путаницами, ошибками и прочими багами. Что же делать? А давайте посмотрим на код.

Решением может являться создание нового надкласса (superClass) с именем, допустим, "Pet" (Любимец). Всех зверюшек (вы можете добавить своих) породим от этого класса и в него же засунем описание всех этих звериных свойств - имя, количество лап, цену и флажок любим/нелюбим

  • name - имя - у всех зверьков есть имя, потому оно должно быть членом класса Pet. Каждый зверь имеет свое имя, поэтому давать его нужно экземпляру. Логично поместить его в конструктор класса Pet, при этом в экземпляре класса создается свойство name, давая право самому экземпляру решать, как оно будет звучать. Если вы хотите, чтобы по умолчанию зверю давалось имя "Безымянный", это значение должно находиться в Pet.prototype, тогда оно будет оставаться таким, пока вы его не замените на нужное.
     


  • legs - лапы - у всех зверей они есть, причем у всех по четыре, поэтому логично это свойство поместить в Рet.prototype со значением 4. Однако перед этим четко определитесь, не решите ли вы в дальнейшем завести себе змею или паука. Если почувствуете, что способны на это, у вас будет три способа решить проблему.
     


    1. Обязанность хранить информацию о собственных лапах возложить на каждое животное. Это делает код легким для понимания и удобно в случае большого разброса количества лап.
    2. Установить по умолчанию в Pet.prototype количество лап, равное четырем и в каждом особом случае переопределять это свойство.
    3. Наконец, при создании экземпляра можно просто передавать в Pet.constructor количество лап как аргумент. Этот путь возлагает кое-какую ответственность на пользователя класса, однако является идеальным решением для какого-нибудь зоомагазина.




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

    5. price - цена - у всех животных она есть, причем у каждого своя. Это свойство должно остаться в Pet.prototype (с возможностью freeware). Еще одно замечание: если различные собаки и коты имеют различные цены, тогда она должна быть установлена в Pet.constructor, подобно имени.
       
    6. pet - любимец/нелюбимец - все зверьки любимцы, с этим все просто.


    7. Последнее, что необходимо сделать, это развернуть прототипы Dog, Cat и Hamster в сторону Pet.prototype вместо Object.prototype:

      Dog.prototype.__proto__ = Pet.prototype; Cat.prototype.__proto__ = Pet.prototype; Hamster.prototype.__proto__ = Pet.prototype;

      Итак, вооружившись всем этим, приступим.



      // Pet class Pet = function( name ) { this.name = name; } Pet.prototype.legs = 4; Pet.prototype.pet = true;

      // Dog class Dog = function( name ){ } Dog.prototype.__proto__ = Pet.prototype; Dog.prototype.price = 10;

      // Cat class Cat = function( name ){ } Cat.prototype.__proto__ = Pet.prototype; Cat.prototype.price = 5;

      // Hamster class Hamster = function( name ){ } Hamster.prototype.__proto__ = Pet.prototype; Hamster.prototype.price = 15;

      Оставим на время наши попытки и убедимся, что это работает (на самом деле это еще не работает, вы можете заметить проблему?).

      rover = new Dog( "Rover" ); fluffy = new Cat( "Fluffy" ); ratboy = new Hamster( "Rat-Boy" );

      for(var i in rover){ trace( i + ":\t" + rover[i] ) } /* output pet: true legs: 4 price: 10 */ for(var i in fluffy){ trace( i + ":\t" + fluffy[i] ) } /* output pet: true legs: 4 price: 5 */ for(var i in ratboy){ trace( i + ":\t" + ratboy[i] ) } /* output pet: true legs: 4 price: 15 */

      Свойство __proto__ выполнило задачу. Свойства в Pet.prototype (legs и pet) стали доступны для каждого объекта. Объекты также получили доступ к свойствам в своих прототипах (price).



      Проблема в том, что свойство name осталось не у дел. Вглядевшись, мы замечаем, что оно ниоткуда недоступно. Что мы сделали не так? Когда мы создавали новый объект, выполнялся код конструктора этого класса. Однако мы засунули свойство name в класс Pet, чтобы не переписывать заново код для каждого животного. Но этот конструктор еще не запускался. Вы можете использовать этот код, однако конструкторы каждого младшего класса останутся пустыми. Есть даже термин для этого - "three-legged-dog" (собака на трех лапах, хромая собака). Необходимо запустить старшие конструкторы, до запуска младших.

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

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

      ... и решается проблема парой строчек кода:

      Pet = function( name ) { this.name = name; } Dog = function( name ){ } Dog.prototype.__proto__ = Pet.prototype;

      rover = new Dog( "Rover" ); // test trace(rover.name); // undefined

      Сначала можно просто попытаться запускать конструктор Pet каждый раз, когда создается экземпляр Dog, просто вызывая его из конструктора Dog, правильно? Пока аплодисменты не стали слишком оглушительными, необходимо успеть сказать, что это не работает. Для понимания этого необходимо быть очень внимательными и осторожными, возможно, вновь представить себе объекты в виде черных ящиков. Вот как это может выглядеть:

      Dog = function( name ) { Pet( name ); } Dog.prototype.__proto__ = Pet.prototype; rover = new Dog( "Rover" );

      Pet() в данном случае вызывается в момент активации экземпляра класса Dog (блок кода под описанием класса). Когда Pet() запускается как метод (не как класс - нигде не видно оператора new), this указывает на то, что его вызывает (в нашем случае экземпляр класса). Как только мы узнаем об активации объектов, мы тут же про них забываем и свойство name пропадает вместе с ними. Что нам нужно сделать, так это запустить конструктор класса Pet, причем из самого экземпляра. Существует два решения проблемы, одно великое и ужасное, другое посложнее, но не такое страшное. Плохо, что нет волшебного суперключевого слова, которое бы нам помогло. Итак, пристегните ремни - первое решение.



        Примечание

      Конструктор фактически ничего не знает о каждом конкретном экземпляре, однако он догадывается о том, что с помощью оператора new создается новый безымянный объект. Проще говоря, он "думает", что this указывает на некий объект, который будет создан.

      Мы знаем, что this в конструкторе Dog указывает на его воплощение - rover (см. Примечание). Мы также знаем, что когда объект вызывает метод, this в методе указывает на вызывающий объект. Что нам мешает превратить Pat в метод экземпляра rover и запускать его оттуда? Другими словами, rover будет заключать в себе код конструктора Pet как одно из своих свойств и мы просто попросим rover запустить свой же метод. Давайте скажем это в третий раз: rover съел, проглотил Pet и теперь он внутри него, оттуда мы его и запустим. Теперь, когда метод(!) Pet запустится, this будет указывать на rover, поэтому все свойства, добавленные через this, добавляются к rover. Когда метод Pet отработает, он нам уже будет ни к чему, его смело можно удалить. Причем, нам совершенно не обязательно называть его Pet, его код должен быть таким же, а имя может быть любым. Таким любым, что вы должны быть абсолютно уверены в том, что нигде его не используете в программе как свойство объекта, ведь мы его совсем скоро удалим. Вполне подойдет имя $_base (просто base не подойдет, иначе все ваши base будут лежать в мусорной корзине). Итак, смотрим:



      Pet = function( name ) { this.name = name; } Dog = function( name ) { // делаем Pet методом экземпляра this.$_base = Pet; // запускаем Pet отсюда, т.к. "this" // эквивалентен экземпляру this.$_base( name ); // освобождаемся от внедренного метода! delete this.$_base; } Dog.prototype.__proto__ = Pet.prototype;

      rover = new Dog( "Rover" ); // проверяем trace(rover.name); // Rover

      Ну наконец-то работает! Есть несколько вещей, которые нужно знать. Первое и главное: весь блок кода, касающийся $_base, ДОЛЖЕН быть самым первым в каждом конструкторе. Это ОЧЕНЬ ВАЖНО. Суть в том, что конструктор запускается сверху вниз, а не снизу вверх. Допустим, вы решили переопределить свойство name в конструкторе Dog и пишете: <this.name = 'Dawg'>; стоит вам это написать до блока $_base, тем самым вы установите имя экземпляра в "Dawg", следующий за ним код переименует его в "rover". Конструктор старшего класса перепишет конструктор младшего.



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

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

      <<

         ООП во Flash 5    >>


      Содержание раздела