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

       

Extends. Специальное ключевое слово


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

  1. Мы должны иметь возможность прикреплять все свойства и методы туда, где им место: свойства класса - к прототипу класса, свойства экземпляра - к экземпляру.
  2. Все свойства и методы в прототипах класса должны быть доступны экземплярам во время их создания.
  3. Свойства в границах одного прототипа должны иметь доступ к методам в границах их прототипов (а также прототипов более высокого уровня) в то время, когда мы их определяем (перед тем как создаются экземпляры).
  4. Когда создаётся экземпляр, конструкторы должны запускаться "сверху вниз" для того, чтобы идентичные имена свойств имели правильное старшинство. Это также даст нам гарантии того, что свойства, установленные в конструкторах более высоких уровней, останутся доступными для конструкторов более низких уровней.
  5. Система должна уметь передавать аргументы "вверх" конструкторам более высоких уровней, причём желательно, чтобы мы могли использовать столько аргументов, сколько нам необходимо - без ограничений.
  6. Система должна поддерживать неограниченное количество уровней наследования.
  7. Сверхкласс должен уметь "справляться" с неограниченным количеством подклассов, у каждого подкласса должно быть не больше одного сверхкласса.
  8. В конечном файле не должно быть никаких конфликтов. Также нужно исключить всякую возможность случайной потери информации - удаление или перезапись системы.
  9. В коде должно быть минимальное количество ссылок. Ссылка на сверхкласс, используемая для установления наследования, должна быть только одна. Мы должны иметь возможность безболезненно менять классы местами.
  10. Система должна использовать стандартный синтаксис "И/ИЛИ" (and/or). Она не должна использовать никаких слов или команд, которые бы могли противоречить коду пользователя. Она также не должна использовать слов, зарезервированных для более поздних версий, насколько это вообще возможно.


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

Первое, что нам нужно, это способ связи одного класса с другим, способ создания взаимоотношений подкласса (то есть дочернего класса) со сверхклассом (то есть родительным классом). Это довольно просто. Всё, что для этого требуется, это специальный метод для передачи двух объектов: объект-имя сверхкласса и объект-имя подкласса. С помощью такого метода мы связываем подкласс со сверхклассом посредством изменения __proto__ - свойства прототипа. Видите, всё действительно просто!

Нам также нужно подумать, как назвать этот специальный метод. Прежде чем придумывать что-либо новое, давайте посмотрим, как обстоит дело с другими объектно-ориентированными системами в подобной ситуации. Есть много типов наследования. Два из них, это так-называемые "ограничивающее наследование" и "расширяющее наследование". Ограничивающее наследование определяет всё, что есть "вверху". Далее оно начинает ограничивать или скрывать элементы по мере того, как вы продвигаетесь "вниз" по экземпляру (например, квадрат - это прямоугольник, ограниченный одной измеряемой стороной). Расширяющее наследование - понятие противоположное ограничивающему наследованию. Здесь изначально верхние классы имеют только самые общие элементы, а более частные добавляются в процессе продвижения к экземпляру. Вообще, в ActionScript, классы получают всё больше и больше элементов по мере того, как становятся подклассами. Таким образом, они дополняются, то есть детализируют свои сверхклассы.

Java использует слово extends в виде SubClass extends SuperClass (подкласс расширяет суперкласс) и это именно то, что подошло бы нам лучше всего. Однако в силу некоторых причин, о которых разговор пойдёт позже, мы ограничены возможностью лишь передавать эти два класса нашему спецметоду, как аргументы. Таким образом, мы можем написать что-нибудь вроде:



extends( SuperClass, SubClass )

К сожалению, догадаться о таком синтаксисе непросто. Его нужно заучить. Мы конечно могли бы поменять местами аргументы и написать extends( SubClass, SuperClass ), но разница была бы небольшая. Просто решим и запомним, что сперва следует высший класс ( extends(Pet, Dog) ) и не будем больше об этом думать. К счастью сам метод оказывается на редкость простым (по крайней мере, до тех пор, пока нам не откроется страшная правда!).

/** Extends ------------------------*/ extends = function( superClass, subClass ) { // связь с родителем subClass.prototype.__proto__ = superClass.prototype; }

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

Вообразите себе следующий сценарий: Жучка очень часто пользуется вашим методом extends, ей это нравится. Однажды она использовала его в клипе, загруженном в _level0. Метод extends был уничтожен, программа не работает, в общем, Жучка за внучку, внучка за бабку, бабка за дедку, дедка за репку - тянут-потянут, вытянуть не могут. Значит, по всей вероятности, _root - не самое подходящее место для спецфункции. Решений может быть несколько и как всегда у всех есть свои "плюсы" и "минусы":

Тиха украинская ночь, но сало нужно перепрятать: Мы можем поместить спецфункию в _level44497.

  • Плюсы: Это просто запомнить, так как это наибольшее простое число перед 0xFFFF (65535). А также нашу спецфункцию можно будет вызывать простым выражением _level44497.extends(Pet, Dog);


  • Минусы: Кто-нибудь всё равно может загрузить клип в _level44497, раз уж это место такое примечательное.


  • Если хочешь получше спрятать, положи на самое видное место: Мы можем запихнуть её в Object.prototype.




    • Плюсы: Её очень легко оттуда вызвать - простым выражением extends( Pet, Dog ) в любом месте и всё сработает. Если ещё немного усложнить выражение, то может получиться даже что-то вроде Dog.extends( Pet ), а это выглядит намного более привлекательно.
    • Минусы: Вот именно, "откуда угодно". После того, как мы так долго учились "класть всё на свои места", теперь у нас появится функция, доступная любому объекту, но имеющая отношение только к классам. Это всё равно, что разрешить заниматься сексом везде, в том числе и в общественных местах - в ресторанах, например. Кроме того, так наша спецфункция станет конфликтовать с циклами for-in (об этом мы поговорим позднее).


    После шведского стола бывает шведский стул: А что если мы поместим её в Function.prototype?


    • Плюсы: Так она будет доступна только функциям, а другим элементам, таким, например, как строка "hello", она доступна не будет. И мы можем использовать всё тот же легко запоминающийся синтаксис: Dog.extends( Pet ). Даже если она и будет доступна методам, которым не положено её видеть, мы всё равно уже решили игнорировать адресное пространство имени метода, а также его прототип, так что это не важно. Ну прямо как в рекламе, помните? "ОН вырастет на целых три дюйма всего за одну неделю"?
    • Минусы: В ActionScript у Function не может быть прототипа. А счастье было так близко! Никогда не верьте рекламе...


    Объект... объект??? объект!!!: Наконец, можно просто "прицепить" её к Object.namespace.


    • Плюсы: Снова есть доступ откуда угодно, если использовать ключевое слово Object. К тому же здесь не будет такой проблемы, как в случае с использованием Object.prototype, когда абсолютно всякий объект может беспрепятственно её наследовать. Кроме того, вы не сможете случайно её удалить. Не будет и конфликта имён, как в случае с _root. Если вы хотите обезопасить свою программы, можете составить её прямо здесь.
    • Минусы: Синтаксис снова усложняется: Object.extends( Pet, Dog ). После такой находки, как Function.prototype - это шаг назад, но шаг не смертельный.




    Ну так "повесим" её на Object. Ничего страшного в этом нет, нужно просто немного привыкнуть. После всей этой канители мы можем наконец-то добавить одно-единственное слово к нашему новому методу extends (если мы и дальше будем продвигаться такими же темпами, то боюсь, что мы ещё долго провозимся):

    /** Extends ------------------------*/ Object.extends = function( superClass, subClass ) { // связь с родителем subClass.prototype.__proto__ = superClass.prototype; }

    Помните, что теперь мы должны всегда использовать выражение Object.extends вместо того, чтобы просто писать extends (или _level0.extends). Хорошо, что мы вспомнили об этом прежде, чем создали 5000 строк кода.

      Памятка по Object.prototype

    Всякий объект имеет связь с Object.prototype по цепи своего прототипа. Что произойдёт, если мы назначим здесь свойство? В этом случае, как вы, наверное, уже догадались, все элементы будут иметь доступ к такому свойству, все, включая и его родителя. Это обстоятельство идёт вразрез с самой концепцией объектно-ориентированного программирования, ведь мы всегда старались упорядочить информацию и функциональность нашей программы, создавая унифицированную логическую систему (объектно-ориентированные языки программирования по определению не могут иметь глобальных переменных!). Если у нас появится фрагмент информации, который необходимо сделать доступным всем типам объектов в вашей программе (включая, например "hello".xxx), то возникает другая проблема. А именно, это свойство будет также доступно всем используемым циклам for-in. Системные методы или свойства, такие как array.pop или object.toString могут иметь особое свойство don't enumerate - они относятся к разряду внутренних и не подчиняются общим правилам. Таким образом, если вы пишите for(var i in Object.prototype), что означает "покажи мне всё, что ты видишь" (enumerate), то не вернётся ничего. Эти свойства действительно находятся там и могут быть использованы, однако они невидны в листе свойств объекта. А вот свойства, которые вы сами добавляете, очень даже видны и, чем выше по цепи вы их добавляете, тем большее количество элементов их видит. Если вы думаете, что было бы очень удобно добавить что-то к Object prototype, то помните, что бесплатный сыр бывает только в мышеловке. Лучше делать всё правильно, а не как проще всего.



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

    Свойства и методы могут принадлежать либо классу, либо экземпляру. Если они принадлежат классу, они идут в прототипе. Иногда у вас может возникнуть надобность устанавливать такие свойства, используя методы в том же прототипе или из прототипа классом выше. Это добавляет проблем. Во-первых, у свойства нет возможности доступа к методу в своём собственном прототипе или в прототипах более высокого уровня, на это способны только экземпляры (да и то только благодаря ключевому слову this). Во-вторых, из-за того, что в первую очередь обычно определяются свойства (и это правильно!) у них нет доступа к методам, которые ещё не определены, потому что форвард-ссылки недопустимы. Если бы мы стали в первую очередь определять методы, тогда у тех из них, которые мы вызываем, не было бы доступа к другим свойствам в прототипе (опять же потому что, возможно, мы их ещё не определили). Так дело не пойдёт! У нас возникло сразу две проблемы: отсутствие доступа к методам во время установления свойств прототипа и отсутствие форвард-ссылок.

    Те, кто думает, что без этих функций можно обойтись, глубоко заблуждаются. Без них единственным выходом из создавшегося положения будет перемещение всех свойств (не методов) в экземпляры. Это типичная книжная рекомендация для пользователей JavaScript и ActionScript и даже для Flash'еров. Однако, поступая таким образом, мы лишаемся многих преимуществ объектно-ориентированного программирования. Это всё равно, что возить санки в гору, но с горы на них не кататься. Просто какой-то эксгибиционизм в безлюдном месте. Программа получается организованной неряшливо и кое-как. Именно так и выглядит большинство программ, написанных на ECMA, где по мере увеличения шкалы воцаряется всё больший и больший хаос. Наследование должно упорядочивать элементы, а не разбрасывать их где попало!



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

    Cat.prototype.petType = "cat"; Cat.prototype.defaultState = ???.setState("sleep"); Cat.prototype.setState = function( state ) { if( state == "sleep" ) return "The " + ???.petType + " is sleeping."; if( state == "eat" ) return "The " + ???.petType + " is eating."; }

    Если делать это таким вот образом Cat.prototype.setState( ), то может, конечно, что-то получиться (хотя в данном случае не получится, догадываетесь почему?). Однако, теперь у вас появилась фиксированная ссылка на определённый участок. Если вы когда-либо измените имя класса, вам гарантировано как минимум утомительное "выкапывание", а иногда и многочасовая отладка. Вы можете возразить, что класс Cat уже фиксирован в левой части выражения (Cat.prototype.xxx), так в чём же проблема?.. Разумеется, ссылка в правой части создаёт намного больше проблем, но дело даже не в этом. Любая ссылка в коде - это плохо, так что вместо того, чтобы оправдываться, оглядываясь на чужие ошибки, лучше честно признать, что в списке наших бед появилась ещё одна. Теперь нам уже будет не так легко переименовывать классы, а ведь в процессе разработки структуры часто возникает необходимость в подобной операции. На худой конец и с этим можно было бы как-то мириться, если бы мы пришли, наконец к решению нашей проблемы. Но если теперь один из этих методов попытается вызвать свойство из своего прототипа (или из прототипа классом выше), как это делают возвратные значения, то всё окончательно запутается. Теперь, если мы напишем return Cat.prototype.petType то до поры, до времени всё это, конечно, будет работать, но что случится, если когда-нибудь свойство petType будет перезаписано? Что если подкласс или экземпляр выдадут нам что-то вроде xxx.petType = "siamese cat"? Тогда, будучи вызван из экземпляра SiameseCat, метод setState вернёт нам значение "The cat is eating", вместо "The siamese cat is eating". Это огромная проблема и нет никакой возможности её решить.



    Вторая проблема - проблема форвард-ссылок. Она тоже возникает в коде, подобном вышеприведённому. Как уже упоминалось ранее, файлы SWF не поддерживают форвард-ссылки. Они не скомпилированы и просто не имеют представления о том, что за всем этим может последовать (а ведь это "последующее" часто бывает даже не загружено на тот момент). Таким образом, мы вызываем метод setState ещё до того, как создали его. Совершенно очевидно, что этот путь тупиковый. Если даже вы и определились уже с именами ваших будущих "детей", кто вам сказал, что они станут отзываться на эти имена? Вот так-то вот. Ну а что если мы просто поставим определения свойств перед методами? Тогда всё почти получится. Да, именно "почти", потому что тогда методы не будут иметь доступ к свойствам, которые ещё не определены. Какие ещё есть идеи? Разделить их на части? Или просто запоминать, "кто куда пошёл"? Всё это ни к чему не приведёт.

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


    • Свойства прототипов и методы не имеют доступа друг к другу в момент создания класса, если только не использовать в коде фиксированных ссылок..
    • Прототипы определяются ссылками на классы (в левой части), что затрудняет работу с такими классами.
    • Свойства не могут вызывать методы того же прототипа, если только не запускать их после определения методов, (что всё равно не решает проблему, поскольку даже в этом случае, в силу отсутствия форвард-ссылок, методы не будут иметь доступа ко всем свойствам).


    Так как же быть?

    Проблема с форвард-ссылками похожа на старый как мир вопрос: "Что было раньше - курица или яйцо?". Мы знаем, что методы нужно определять раньше свойств. Мы так же знаем, что свойства нужно определять раньше методов. К счастью, на этот случай есть маленькая хитрость. Методы действительно должны быть определены первыми, но это не означает, что мы должны запускать их первыми. А вот свойства действительно должны быть обработаны в первую очередь. Вот ещё небольшая, но существенная уловка, прототип, как и любой другой элемент, это не класс, а объект. Мы знаем, что по сути объекты, это экземпляры, созданные из образца. А значит, мы можем создать образец, который будет определять прототип, точно так же, как мы делали в случае с другими экземплярами. Таким образом, все свойства и методы определяются в первую очередь, прежде, чем их вызывают. Если мы создадим такой образец внутри прототипа, то при его вызове ключевое слово this будет приравнено к прототипу класса. Так что все эти свойства и методы будут скопированы в прототип. Огромное преимущество этого способа заключается в том, что ключевое слово this приравнивается к прототипу, пока подготавливаются методы и свойства. Это означает, что свойства будут иметь доступ к методам, а методы в свою очередь будут иметь доступ к свойствам. Чтобы быть уверенным, что методы определяются до того, как свойства их вызывают, можно просто разделить их (методы и свойства) на две группы и первыми запускать методы. Потеряют ли в этом случае методы доступ к свойствам? Нет. Потому, что, как мы помним, методы в это время только определяются, а не запускаются. Таким образом, несмотря на то, что к моменту определения метода, свойство, которое он вызывает, ещё не создано, оно будет создано к тому моменту, как будет вызван метод. А как насчёт экземпляров? Если методы ссылаются на другие свойства в прототипе, не означает ли это, что, когда мы создаём экземпляр данного класса, он будет напрямую ссылаться на прототип? Нет. Потому, что значение слова this изменяется в момент доступа из экземпляра. Будучи вызвано из прототипа класса, оно ссылается на прототип. При вызове метода класса из экземпляра, this будет ссылаться на экземпляр. Красотища!



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

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

    // Класс B = function( ){ } // Свойства B.prototype.classProperties = function ( ) { this.prop1 = 55; this.prop2 = this.double( this.prop1 ); } // Методы B.prototype.classMethods = function ( ) { this.double = function ( ) {return this.prop1 * 2;} } // так B становится подклассом A Object.extends( A, B );

    Так как же тогда будет выглядеть наш специальный метод 'extends'? Попробуйте отследить его работу в нижеприведённом коде, игнорируя (пока что) ту часть, которая связана с customKeyword.

    // Extends ------------------------ Object.extends = function( superClass, subClass ) { // связываем с customKeyword, если это верхний уровень // работает, даже если позднее добавить более высокие уровни if( (superClass.prototype.__proto__ == Object.prototype) && (superClass.prototype <> Object.customKeyword.prototype) ) { superClass.prototype.__proto__ = Object.customKeyword.prototype; // устанавливаем прототип сверхкласса, если он ещё не установлен if( typeof(superClass.prototype.classMethods) != undefined ) { superClass.prototype.classMethods(); delete superClass.prototype.classMethods; } if( typeof(superClass.prototype.classProperties) != undefined ) { superClass.prototype.classProperties(); delete superClass.prototype.classProperties; } }



    // связь с родителем subClass.prototype.__proto__ = superClass.prototype

    // Устанавливаем прототип подкласса. Чтобы избежать конфликтов // или повторного запуска, эти методы должны быть // удалены после того, как они своё отработают if( typeof(subClass.prototype.classMethods) != undefined ) { subClass.prototype.classMethods(); delete subClass.prototype.classMethods; } if( typeof(subClass.prototype.classProperties) != undefined ) { subClass.prototype.classProperties(); delete subClass.prototype.classProperties; } }

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

    <<

       ООП во Flash 5    >>


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