|
|
Книги-onlineООП во Flash 5: Extends Extends. Специальное ключевое словоТеперь, когда мы уже так много узнали, пришло время решить, чего же мы хотим. Все наши новые концепции мы попытаемся теперь выстроить в одну простую и логичную систему. После того, как нам несколько раз не удастся этого добиться, мы будем вынуждены остановиться на некоторых новых ключевых словах. На первый взгляд они могут показаться немного непонятными, но, в конце концов, помогут достичь нашей цели. Итак, какой бы мы хотели видеть нашу систему? Всё это несколько пугает, но большинство из вышеперечисленного мы уже обговорили и рассмотрели. Теперь осталось только всё это автоматизировать. Если вам неинтересно или вы неспособны "переварить" схему работы этой системы, то, наверное, вы будете счастливы узнать, что можно просто пропустить этот раздел, и, несмотря на такой значительный "пробел", дочитать книгу до конца и создавать полнофункциональные объектно-ориентированные программы. Первое, что нам нужно, это способ связи одного класса с другим, способ создания взаимоотношений подкласса (то есть дочернего класса) со сверхклассом (то есть родительным классом). Это довольно просто. Всё, что для этого требуется, это специальный метод для передачи двух объектов: объект-имя сверхкласса и объект-имя подкласса. С помощью такого метода мы связываем подкласс со сверхклассом посредством изменения __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.
Если хочешь получше спрятать, положи на самое видное место: Мы можем запихнуть её в Object.prototype.
После шведского стола бывает шведский стул: А что если мы поместим её в Function.prototype?
Объект... объект??? объект!!!: Наконец, можно просто "прицепить" её к Object.namespace.
Ну так "повесим" её на Object. Ничего страшного в этом нет, нужно просто немного привыкнуть. После всей этой канители мы можем наконец-то добавить одно-единственное слово к нашему новому методу extends (если мы и дальше будем продвигаться такими же темпами, то боюсь, что мы ещё долго провозимся): /** Extends ------------------------*/ Object.extends = function( superClass, subClass ) { // связь с родителем subClass.prototype.__proto__ = superClass.prototype; } Помните, что теперь мы должны всегда использовать выражение Object.extends вместо того, чтобы просто писать extends (или _level0.extends). Хорошо, что мы вспомнили об этом прежде, чем создали 5000 строк кода.
Другая проблема, связанная с использованием метода 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
>>
Внимание! Если у вас не получилось найти нужную информацию, используйте рубрикатор или воспользуйтесь поиском . книги по программированию исходники компоненты шаблоны сайтов C++ PHP Delphi скачать |
|