Январь 2011

Руководство по actionscript. часть 1, стр. 110

? Методы, описанные с помощью атрибута fin а 1, помогают скрыть детали внутренней реализации класса. Если описать класс или метод, используя этот атрибут, другие программисты не смогут расширить такой класс или перекрыть метод с целью изучения внутренней структуры класса. Такая мера предосторожности является одним из способов защиты приложения от вредоносного кода.

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

Создание подклассов внутренних классов

Точно так же, как мы создаем подклассы для наших собственных классов, мы можем создавать подклассы для любого внутреннего класса, описанного без использования атрибута final, что позволит реализовать специализированную функциональность на базе существующего класса языка ActionScript. Пример расширения внутреннего класса Array можно найти в разделе Programming ActionScript 3.0 > Core ActionScript 3.0 Data Types and Classes > Working with Arrays > Advanced Topics документации по программированию на языке ActionScript 3.0 корпорации Adobe. Пример расширения внутреннего класса Shape среды выполнения Flash можно найти в разд. «Пользовательские графические классы» гл. 20.

Руководство по actionscript. часть 1, стр. 111

Некоторые внутренние классы языка ActionScript представляют собой простые коллекции методов и переменных класса, например классы Math, Keyboard и Mouse существуют только для хранения связанных методов и переменных (например, Math. random ( ) и Keyboard. ENTER). Такие классы называют библиотеками статических методов. Они объявляются в основном с использованием атрибута final.

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

Теория наследования

166

Теория наследования

До настоящего момента основное внимание уделялось рассмотрению деталей практического применения наследования в языке ActionScript. Однако теория о том, где и когда применять наследование, гораздо глубже технической реализации. Рассмотрим несколько основных теоретических принципов, принимая во внимание тот факт, что для освещения этой темы целиком нескольких страниц явно недостаточно. Более подробное описание теории наследования можно найти по адресу http://archive. eiffelxom/doc/manuals/technology/oosc/inheritance-design^ page. html. Страница представляет собой онлайновую выдержку из книги «Object-Oriented Software Construction (издательство Prentice Hall) Бертранда Мейера (Bertrand Meyer).

Руководство по actionscript. часть 1, стр. 112

Почему наследование?

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

Кроме того, наследование позволяет выражать архитектуру приложения в иерархических терминах, отражающих реальный мир и человеческую психологию. Например, в реальном мире мы считаем, что растения отличаются от животных, но в то же время и тех и других мы относим к живым существам. Мы считаем, что автомобили отличаются от самолетов, но и те и другие являются средством передвижения. Соответствующим образом в приложении для управления кадрами может существовать суперкласс Employee с подклассами Manager, CEO и Worker. В банковском приложении можно создать суперкласс BankAccount с подклассами CheckingAccount и SavingsAccount. Все эти канонические примеры демонстрируют одну из разновидностей наследования, иногда называемую наследованием подтипов, когда иерархия классов приложения моделирует ситуацию в реальном мире (называемую доменом или проблемной областью).

Несмотря на то что примеры классов Employee и BankAccount демонстрируют привлекательные возможности наследования, далеко не каждое наследование отражает реальный мир. На самом деле чрезмерный акцент на моделировании реального мира может привести к неправильному пониманию наследования и, как следствие, к его неправильному использованию. Например, в случае с классом Person мы могли бы поддаться искушению и создать подклассы Female и Male. В реальном мире данные категории являются логичными, но если бы эти классы использовались, скажем, в приложении для генерации отчетов в учебном заведении, нам пришлось бы создать классы MaleStudent и Female Student только для того, чтобы сохранить иерархию реального мира. В нашей программе операции,

Руководство по actionscript. часть 1, стр. 113

Описанные для студентов мужского пола, ничем не отличаются от операций, описанных для студентов женского пола, и, следовательно, должны использоваться одинаково. В данном случае иерархия реального мира конфликтует с иерархией нашего приложения. Если вам необходима информация о поле, лучше создать один класс Student и добавить переменную gender в класс Person. Насколько бы это ни было заманчивым, мы должны избегать создания структур наследования, основываясь исключительно на реальном мире, а не на требованиях нашей программы.

И наконец, помимо возможности повторного использования кода и создания логической иерархии, наследование позволяет применять различные типы объектов там, где требуется только один тип. Эта важная возможность, называемая полиморфизмом, заслуживает отдельного рассмотрения.

Полиморфизм и динамическое связывание

Полиморфизм — это возможность, присущая всем настоящим объектно-ориентированным языкам программирования, которая заключается в том, что экземпляр подкласса может быть использован везде, где допустимо применение экземпляра его суперкласса. Само по себе слово «полиморфизм» буквально обозначает «множество форм» — любой объект можно рассматривать как экземпляр собственного класса или как экземпляр любого из его суперклассов.

Напарником полиморфизма является динамическое связывание, гарантирующее, что в результате вызова метода над объектом будут выполнены именно те инструкции, которые определены в фактическом классе данного объекта.

В качестве канонического примера полиморфизма и динамического связывания можно привести графическое приложение, отображающее фигуры на экране. В этом приложении определен класс Shape с нереализованным методом draw ( ):

public class Shape { public function draw ( ) { // Реализация метода отсутствует. В некоторых других языках // метод draw( ) был бы объявлен с помощью атрибута abstract, // который синтаксически обязует подклассы класса Shape // предоставить реализацию данного метода.

}

}

Класс Shape имеет несколько подклассов — Circle, Rectangle и Triangle, каждый из которых предоставляет собственное описание метода draw ( ):

public class Circle extends Shape { override public function draw ( ) { // Код для отрисовки окружности на экране не показан…

}

}

public class Rectangle extends Shape { override public function draw ( ) {

// Код для отрисовки прямоугольника на экране не показан…

}

}

public class Triangle extends Shape { override public function draw ( ) { // Код для отрисовки треугольника на экране не показан…

}

}

Чтобы нарисовать новую фигуру на экране, мы передаем экземпляр класса Circle, Rectangle или Triangle в метод addShape ( ) основного класса приложения DrawingApp. Код метода addShape ( ) класса DrawingApp выглядит следующим образом:

public function addShape (newShape) { newShape. draw( );

// Оставшаяся часть метода (код не показан) занималась бы добавлением // новой фигуры во внутренний список фигур, отображаемых // на экране

}

Теперь рассмотрим пример добавления фигуры, представленной классом Circle, на экран:

drawingApp. addShape(new Circle( )):

Метод addShape ( ) вызывает метод draw ( ) для новой фигуры и добавляет эту фигуру во внутренний список фигур, отображаемых на экране. И самое главное — метод addShape ( ) вызывает метод draw ( ), не зная (и не заботясь о том), экземпляром какого класса является новая фигура — Circle, Rectangle или Triangle. Благодаря процессу динамического связывания, происходящему на этапе выполнения программы, среда Flash использует подходящую реализацию данного метода. Иными словами, если новая фигура является экземпляром класса Circle, то среда выполнения Flash вызовет метод Circle. draw ( ); если новая фигура является экземпляром класса Rectangle, то Flash вызовет метод Rectangle. draw ( ); если же новая фигура является экземпляром класса Triangle, то среда Flash вызовет метод Triangle. draw ( ). Важно отметить, что на этапе компиляции конкретный класс добавляемой фигуры неизвестен. По этой причине динамическое связывание часто называют поздним связыванием: вызов метода связывается с конкретной реализацией «с опозданием» (то есть на этапе выполнения).

Руководство по actionscript. часть 1, стр. 114

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

public class Circle extends Shape { public function drawCircle ( ) {

// Код для отрисовки окружности на экране не показан…

}

}

public class Rectangle extends Shape { public function drawRectangle ( ) { // Код для отрисовки прямоугольника на экране не показан…

}

}

public class Triangle extends Shape { public function drawTriangle ( ) { // Код для отрисовки треугольника на экране не показан…

}

}

Далее внутри метода addShape ( ) класса DrawingApp нам бы пришлось использовать оператор is, чтобы вручную определять класс каждой новой фигуры и вызывать подходящий метод для отрисовки, как показано в следующем коде. Оператор is возвращает значение true в том случае, если указанное выражение принадлежит заданному типу данных; в противном случае возвращается значение false. Типы данных и оператор is будут рассмотрены в гл. 8.

public function addShape (newShape) { if (newShape is Circle) {

newShape. drawCircle( ); } else if (newShape is Rectangle) {

newShape. drawRectangle( ); } else if (newShape is Triangle) {

newShape. drawTriangle( );

}

// Оставшаяся часть метода (код не показан) занималась бы добавлением // новой фигуры во внутренний список фигур, отображаемых на экране

}

Уже сейчас очевидны трудности выбранного подхода. Теперь представьте, что произойдет, если мы добавим 20 новых типов фигур. Для каждого нового типа нам придется вносить изменения в метод addShape( ). В мире, где существует полиморфизм, нам не пришлось бы изменять код, вызывающий метод draw ( ) над каждым экземпляром класса Shape. Поскольку каждый подкласс класса Shape предоставляет собственное подходящее описание метода draw ( ), наше приложение будет «просто работать» без необходимости внесения других изменений.

Руководство по actionscript. часть 1, стр. 115

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

Наследование в сравнении с композицией

Композиция, представляющая альтернативный вид отношений между объектами, зачастую соперничает с наследованием в качестве методики объектно-ориентиро-

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

// Внутренний класс является аналогом суперкласса в наследовании public class BackEnd {

public function doSomething ( ) {

}

}

// Внешний класс является аналогом подкласса в наследовании public class FrontEnd {

// Экземпляр внутреннего класса сохраняется в закрытой переменной

// экземпляра, в данном случае в закрытой пременной be

private var be;

// Конструктор создает экземпляр внутреннего класса public function FrontEnd ( ) { be = new BackEnd( );

}

// Этот метод поручает работу методу doSomething( ) класса BackEnd public function doSomething ( ) { be. doSomething( );

}

}

Обратите внимание, что класс FrontEnd не расширяет класс BackEnd. Композиция не требует использования собственного особого синтаксиса, как это происходит с наследованием. Более того, внешний класс может использовать подмножество методов внутреннего класса, все методы или добавлять собственные несвязанные методы. Имена методов во внешнем классе могут полностью совпадать с именами методов во внутреннем классе или совершенно отличаться. Внешний класс может ограничивать, расширять или переопределять возможности внутреннего класса, играя такую же роль, как подкласс в наследовании.

Ранее в этой главе было описано, как с помощью наследования класс Square может ограничивать поведение класса Rectangle. В листинге 6.2 показано, как одно и то же отношение между классами может быть реализовано с использованием композиции вместо наследования. В предлагаемом коде класс Rectangle остался неизмененным. Однако на этот раз класс Square не расширяет Rectangle. Вместо этого в классе Square описана переменная г, содержащая экземпляр класса Rectangle. Фильтрация всех операций над экземпляром, хранящимся в переменной г, происходит через public-методы класса Square. Класс Square переадресует, или делегирует, вызовы методов экземпляру, хранящемуся в переменной г. Обратите внимание, что, поскольку метод setSize ( ) класса Square не перекрывает метод setSize ( ) класса Rectangle, сигнатура метода setSize ( ) класса Square не обязана совпадать с сигнатурой метода setSize ( ) класса Rectangle. В методе setSize ( ) класса Square можно определить один-единственный параметр, в отличие от метода setSize ( ) класса Rectangle, в котором определено два параметра.

Руководство по actionscript. часть 1, стр. 116

Листинг 6.2. Пример отношения композиции

// Класс Rectangle public class Rectangle {

protected var w = 0;

protected var h = 0;

public function Rectangle (width, height) { setSize(width, height);

}

public function setSize (newW, newH) { w = newW; h = newH;

}

public function getArea ( ) { return w * h;

}

}

// Это новый класс Square public-class Square { private var r;

public function Square (side) { r = new Rectangle(side, side);

}

public function setSize (side) { r. setSize(side, side);

}

public function getArea ( ) { return r. getArea( );

}

}

Отношения «является», «имеет» и «использует». В разговорной речи отношение наследования, присущее объектно-ориентированным языкам программирования, называется отношением «является» (Is-А), поскольку экземпляр подкласса в буквальном смысле можно рассматривать как экземпляр его суперкласса (то есть экземпляр подкласса может быть использован везде, где это допустимо применением экземпляра его суперкласса). В предыдущем примере полиморфизма экземпляр класса Circle «является» экземпляром класса Shape, поскольку класс Circle унаследован от Shape и, следовательно, может использоваться везде, где используется Shape.

Отношение композиции называется отношением «имеет», поскольку внешний класс содержит экземпляр внутреннего класса. Не следует путать отношение «имеет» с отношением «использует», когда некоторый класс создает объект другого класса, но не присваивает созданный объект переменной экземпляра. В отношении «использует» класс использует объект, а затем выбрасывает его. Например, класс Circle мо-

жет хранить числовое значение цвета в переменной color («имеет» объект класса uint), но впоследствии может временно воспользоваться объектом класса Color, чтобы отобразить этот цвет на экране («использует» объект класса Color).

В листинге 6.2 класс Square «имеет» экземпляр класса Rectangle п налагает на него ограничения, которые фактически превращают класс Rectangle в Square. В случае с классами Square и Rectangle отношение «является» выглядит более естественным, однако можно использовать и отношение «имеет». В этой связи возникает вопрос: какое отношение лучше?

Руководство по actionscript. часть 1, стр. 117

Когда использовать композицию вместо наследования. Листинг 6.2 поднимает серьезный вопрос проектирования. Как сделать выбор между композицией и наследованием? Вообще, достаточно легко определить ситуацию, в которой наследование неприменимо. Экземпляр класса Alert Dialog в приложении «имеет» кнопку ОК, но сам экземпляр класса AlertDialog кнопкой ОК не «является». Сложнее определить ситуацию, когда неприменимой оказывается композиция, поскольку наследование, используемое для создания отношения между двумя классами, всегда можно заменить композицией. Если в одной и той же ситуации применимы оба подхода, какой из них окажется лучшим выбором?

Для новичков в объектно-ориентированном программировании будет неожиданностью услышать, что зачастую при выборе стратегии проектирования приложений предпочтение отдают композиции, а не наследованию. На самом деле некоторые известные теоретики в области объектно-ориентированного проектирования недвусмысленно советуют использовать композицию вместо наследования (книга «Design Patterns: Elements of Reusable Object-Oriented Software» издательства Addison-Wesley, авторы Эрих Гамма (Erich Gamma) и др.). Таким образом, здравый смысл подсказывает нам хотя бы рассмотреть возможность применения композиции даже в том случае, когда выбор наследования кажется очевидным. И все-таки, вот несколько общих рекомендаций, которые помогут сделать выбор между наследованием и композицией:

? если вы желаете воспользоваться преимуществом полиморфизма, рассмотрите возможность применения наследования;

? когда классу необходимы сервисы другого класса, рассмотрите возможность использования отношения композиции;

? если поведение разрабатываемого вами класса очень похоже на поведение существующего класса, рассмотрите возможность применения отношения наследования.

Дополнительные советы по выбору подхода проектирования между композицией и наследованием можно найти в прекрасной статье JavaWorld Билла Веннера (Bill Venner), которая хранится в архиве на сайте автора: http://www. artima. com/ designtechniques/compoinh. html. Мистер Веннер приводит неоспоримые доказательства, что:

Руководство по actionscript. часть 1, стр. 118

? изменение кода, использующего композицию, влечет за собой меньше последствий, чем изменение кода, использующего наследование;

? код, основанный на наследовании, зачастую выполняется быстрее, чем код, в основе которого лежит композиция.

Отсутствие поддержки абстрактных классов и методов

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

Абстрактные классы являются широко распространенной, важной частью проектов, в которых применяется полиморфизм. Например, ранее при обсуждении полиморфизма мы рассмотрели класс Shape и его подклассы Circle, Rectangle и Triangle. В обычной ситуации метод draw ( ) класса Shape был бы объявлен абстрактным методом, гарантируя, что:

? каждый подкласс класса Shape предоставляет средства для его отображения на экране;

? внешний код может безопасно вызывать метод draw ( ) над любым подклассом класса Shape (поскольку компилятор не позволит классу расширить класс Shape, не реализовав метод draw ( ) ).

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

Руководство по actionscript. часть 1, стр. 119

В большинстве случаев для реализации конкретной объектно-ориентированной архитектуры вместо абстрактных классов могут быть использованы интерфейсы языка ActionScript. О том, что такое интерфейсы, вы прочтете в гл. 9.

Мы рассмотрели понятие наследования. В конце этой главы применим полученные знания к программе по созданию виртуального зоопарка.

Применение наследования в программе по созданию виртуального зоопарка

В нашей программе «Зоопарк» наследование будет добавлено для решения двух различных задач. Во-первых, мы используем его для описания видов пищи, поглощаемой нашими животными, тем самым, заменив предыдущий подход, заключав-

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

Создание видов пищи

До этого момента наша реализация процесса принятия пищи в программе по созданию виртуального зоопарка была чрезмерно упрощена. Чтобы накормить животное, мы просто вызывали метод eat ( ) над желаемым объектом класса VirtualPet и указывали количество калорий, поглощаемое животным. Для реализма нашей имитации добавим в программу зоопарка виды пищи.

Чтобы не усложнять процесс, мы позволим животному принимать только два вида пищи: суши и яблоки. Суши будут представлены новым классом Sushi, а яблоки — классом Apple. Поскольку оба класса — Sushi и Apple — концептуально представляют пищу, они будут иметь почти одинаковую функциональность. Следовательно, в нашем приложении мы реализуем всю функциональность, необходимую обоим классам Sushi и Apple, в одном суперклассе Food. Классы Sushi и Apple расширят класс Food и через наследование получат доступ к его возможностям.

Класс Food описывает четыре простых метода для получения и изменения имени и значения калорий для заданного продукта. Рассмотрим его код:

package zoo { public class Food { private var calories; private var name;

public function Food (initialCalories) { setCalories(initial Calories);

}

public function getCalories ( ) { return calories;

}

public function setCalories (newCalories) { calories = newCalories;

}

public function getName ( ) { return name;

}

public function setName (newName) { name = newName;

}

Класс Apple задает количество калорий, используемое но умолчанию, для каждого объекта Apple и определяет название продукта для всех объектов Apple. Рассмотрим его код:

package zoo { public class Apple extends Food { // Количество калорий, используемое по умолчанию, для объекта // Apple равно 100

private static var DEFAULT_CALORIES = 100:

public function Apple (initialCalories = 0) { // Если для данного конкретного объекта количество калорий не указано // или если было указано отрицательное число… if (initialCalories <= 0) {

// …использовать значение по умолчанию

initialCalories = Applе. DEFAULT_CALORIES;

}

super(i niti alCalori es);

// Определить название продукта для всех объектов Apple setNameCApple»);

}

}

}

Класс Sushi задает количество калорий, используемое по умолчанию, для каждого объекта Sushi и определяет название продукта для всех объектов Sushi. Рассмотрим его код:

package zoo { public class Sushi extends Food { private static var DEFAULT_CALORIES = 500;

public function Sushi (initialCalories = 0) { if (initialCalories <= 0) { initialcalories = sushi.default_calories;

}

super(initialCalories); setName(«Sushi»);

}

}

}

Чтобы объекты класса VirtualPet могли есть яблоки и суши, мы должны модифицировать метод eat ( ) класса VirtualPet. Вот как выглядел метод eat ( ) до настоящего времени:

public function eat (numberOfCalories) { if (currentCalories == 0) { trace(getName( ) + » is dead. You can’t feed it.»); return;

var newCurrentCalories = currentCalories + numberOfCalories; if (newCurrentCalories > VirtualPet. maxCalories) {

currentCalories = Virtual Pet. maxCalories; } else {

currentCalories = newCurrentCalories;

}

trace(getName( ) + » ate some food. It now has » + currentCalories + » calories remaining.»);

}

В новой версии метода eat ( ) мы переименуем параметр numberOfCalories в foodltem, введя тем самым логическое соглашение, что аргументом метода eat ( ) должен быть экземпляр любого класса, унаследованного от класса Food (в гл. 8 будет рассмотрено, как обеспечить выполнение этого соглашения с помощью объявления типа). Внутри метода eat ( ) значение переменной newCurrentCalories будет вычисляться путем сложения значения калорий принимаемого куска пищи (то есть foodltem. getCalories( ))и существующего количества калорий у животного (то есть currentCalories). Наконец, при выводе информации о том, что животное съело пищу, мы воспользуемся методом getName ( ) класса Food, чтобы указать название съеденной пищи. Рассмотрим измененный метод eat ( ):



Полезные ссылки
Случайные записи
  • 19.02.2014">О пользе массажа
  • 24.02.2011">Руководство по actionscript. часть 7, стр. 014
  • 22.01.2011">Руководство по actionscript. часть 1, стр. 131
  • 18.05.2010">Самоучитель по креативному веб-дизайну. Книга 2, стр.42
  • 19.03.2011">Руководство по actionscript. часть 2, стр. 091
  • 25.02.2011">Руководство по actionscript. часть 6, стр. 097
  • 09.05.2010">Самоучитель по креативному веб-дизайну. Книга 1, стр.136
  • 22.03.2011">Руководство по actionscript. часть 2, стр. 012
  • 12.03.2011">Руководство по actionscript. часть 3, стр. 126
  • 24.02.2011">Руководство по actionscript. часть 6, стр. 106
  • 23.01.2011">Руководство по actionscript. часть 1, стр. 091
  • 22.01.2011">Руководство по actionscript. часть 1, стр. 107
  • 20.07.2011">Как не обмануться, выбирая CMS (часть 1)
  • 18.05.2010">Самоучитель по креативному веб-дизайну. Книга 2, стр.102
  • 15.03.2011">Руководство по actionscript. часть 3, стр. 043
Опрос

Какие цвета вы предпочитаете?

View Results

Loading ... Loading ...