Вступление

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

Начнём с вещей, которые в программерском мире часто рассматриваются в качестве гипотетических примеров: с фигур, кругов и квадратов. Ещё бы, для счастливцев, реально работающих с Flash, вышеперечисленные понятия уже не какая-нибудь абстракция, а, напротив, самые фундаментальные вещи в мире. Выходит, с них и начинать. Мы выстроим законченную систему, способную стать базой для полнофункциональных приложений... Ну ладно, ладно, не придирайтесь, полнофункциональных приложений в рамках Flash. Зато, в пику всяким ограничениям, вы, как флэш-кодер, сможете сделать множество вещей, которые неподвластны обычным программистам. Например, порисовать. Ха! И пусть попробуют поспорить!

Начнём с реального и полезного (не путать с реально полезным) примера работы с Формой (Shape).

<< ООП во Flash 5 ( II ) >>


Процесс

Программисты - чёртовы лгуны! Нет, я не имею в виду то, как они обманывают своих домовладельцев или враки про их образование, всё гораздо хуже. Они не спят по ночам и неделями выдумывают, выискивают, как бы навернуть что-нибудь такое-эдакое, ужасно сложное, какую-нибудь хитромудрую ООП-структуру или ещё что... Наконец они запихивают это всё в несколько классов с красивой иерархией и сотней-другой строк кода. Результат выглядит потрясающе. Затем, в своих книгах они обычно пишут, что если необходимо решить проблему Х, то нужно использовать именно эту структуру, потому как именно она бесспорно лучшая и невероятно прозрачная. Ну, или не пишут, а просто приводят как само собой разумеющееся свой прилизанный чудо-код. Все думают, что они гении, у вас же остаётся чувство неадекватности собственных классов, ибо они никогда не получаются красивыми ни сразу, ни даже... после. Хуже всего то, что решить проблему Х вы можете, но совершенно без понятия, как решать остальные проблемы и чувствуете себя совершенным ничтожеством. Остаётся лишь малое утешение, что тот программер втайне чувствует себя точно также. Ведь это все остальные программисты могут легко придумать замечательную структуру во сне, тогда как ему или ей приходится не спать ночами, барахтаясь в море смятых бумажек, попытка за попыткой... В результате, гнетущее чувство неполноценности делает их изгоями, которых коллеги воспринимают лишь из снисходительности. И даже модный костюм не может помочь их горю.

Мораль сей басни такова: создание ОО-программ - процесс многократной переделки одного и того же. Вы создали структуру - получилось отстойно. Выбрасываете её и создаёте другую, уже чуть менее отстойную. Но! Вот эту часть надо бы выкинуть, а то свойство переместить бы на уровень выше, а вот те, сделать подклассами. Наконец картинка складывается перед глазами и вы ещё раз убеждаетесь... это вновь отстой. Но теперь вы стали умнее, вы перелопачиваете весь код, искусно пользуясь клипбордом, выравниванием и, в конце концов, всё работает, всё прозрачно! "Ай, да я! Мне пора писать книги!", - думаете вы. В конечном счёте, становитесь всё опытнее, используете накопленные черновики и много думаете над усовершенствованиями. Экономит ли это время или нет, вопрос спорный, но шансов, что другой программист найдёт один из таких черновиков с вашими бредовыми идеями и посмеется над вами, гораздо меньше. Не убедил? Вот цитата из Бертрана Мейера, создателя языка Eiffel, гениальнейшего из гениальнейших OO-умников:

"..you don't need iterations, its easy..." ("...нет необходимости повторять, это легко...") - найдите эту цитату!

Хм, здорово, видимо, парням такого уровня не стоит заморачиваться подобной ерундой... Конечно, он написал немало пухлых книжек и даже придумал язык программирования. Верить или нет подобным утверждениям столь авторитетных личностей?

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


Настоящий Класс ( Class )

То, над чем мы будем работать - простенькая программка, позволяющая отображать фигуры и управлять ими. Хотя процесс создания OO-программы обычно включает в себя множество итераций и неудавшихся моделей, вы пытаетесь, до того как продолжите работу, выявить для себя проблему как можно яснее. Однако, дабы избегнуть нагромождения кода на первой же странице, давайте просто строить то, что строим, и по ходу дела добавлять то, что надо добавить. Терпение и труд, господа.

Итак, наша цель - программа управления фигурами, поэтому начнём с класса Фигур (Shape). На самом низком уровне наделим фигуры способностью двигаться и изменять размер. В псевдокоде (упрощённый код, предназначенный исключительно для людей, компьютер бы просто умер, попытайся его разгадать) это будет выглядеть примерно так:

// Shape Class ( класс Shape )
Shape(x,y,w,h)
{
// set x,y,w,h
}
// Свойства
x,y,w,h
// Методы
Move(x,y);	// переместить форму в позицию с координатами x y
Size(w,h);	// установить ширину в w, высоту в h
// Теперь мы можем это использовать:
s1 = new Shape( 10,20,30,40 );
s1.Move( 20, 30 );
s1.Size( 100, s1.y );

Однако остаются проблемы. А пока запомните одну важную вещь. Зарубите её себе на носу! (Если там ещё осталось место для памятных зарубок):

Когда создаётся класс, нужно предоставить пользователю возможно более простую, внутренне непротиворечивую, логичную структуру.

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

В чём проблема с первой строкой?

s1 = new Shape( 10,20,30,40 );

Первый очевидный вопрос: что обозначают все эти числа? Разумеется, можно поискать где-то вверху, но не забывайте, что между описанием класса и его использованием может быть достаточно большой кусок кода. А что будет, если добавить ещё несколько свойств - вращение, цвет, скос, имя?.. Следующий вопрос: что будет, если мы зададим ширину и высоту, а первые два параметра, x и y, оставим заданными по умолчанию? Если даже пользователь впишет null в необходимые позиции, значения установятся в 0, что может отличаться от значений по умолчанию. Другой вариант - передать лишь значения ширины и высоты, вновь создаёт проблему. Как объяснить классу Shape разницу между определением значений ширины, высоты и значений x и y? В конце концов, и те и другие всего лишь пары целых чисел. Это всё равно что, стоя на углу какой-нибудь улицы Ленина, гадать, в каком городе находитесь.

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

w = CreateWindow ("button","Click Me",WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
    100,100,50,50,hWnd, (HMENU) 100, ((LPCREATESTRUCT)
    lParam)->hInstance,NULL);

В то время, а ведь, возможно, всё это до сих пор хранится где-то глубоко в недрах ОС, видимо, так было нужно, ныне же нам гораздо приятнее смотреть на современный конструктор (взято из .NET, но подобным образом работает и Java, и KDE):

button1 = new Button();
button1.BackColor = SystemColors.Desktop;
button1.Size = new Size (176, 56);
button1.Location = new Point (168, 168);
button1.Text = "Click Me";

Первый пример наглядно демонстрирует, почему атомные реакторы не работают под Windows 3.1. Пораскинув мозгами, из этого можно извлечь несколько полезных уроков. Во-первых, если серьёзных причин для усложнений системы нет, передавайте конструктору только те параметры, которые ему необходимы для создания объекта. Довольно заманчиво выглядит возможность разрешить пользователю передавать параметр Text как аргумент; но тогда почему Text должен быть важнее, чем, к примеру, Location; если же передаются оба параметра, то какой должен передаваться первым? И вообще, позволит ли нам передача этих параметров как аргументов сделать что-либо, кроме того, что задано жёстко (разве что людей запутать?). Короткий ответ: "Нет". Длинный ответ: "Этому не бывать!". Отсюда - правило: если параметр не нужен для создания объекта, не передавайте его в конструктор. (Чуть позже мы рассмотрим "перегрузку конструктора" - общепринятую, хотя и несколько противоречивую технологию, рассматривающую данную проблему с другой позиции.)

Прежде чем продолжить, взглянем ещё раз на вторую строку нашего кода:

s1.Move( 20, 30 );

Во-первых, что это означает? Что? Нужно ли переместиться в позицию с координатами 20, 30, или нужно сместиться на 20 единиц вправо и на 30 вниз? Узнать это можно лишь одним способом, найти в описании класса, который я вам навязал. Проверили? Нашли? Теперь же, найдя не только это, но и дорогу назад (забыв, правда, что там искали), вы, наконец, поняли, что первоначально это не было задано в классе. Прежде чем начать грязно ругаться, запомните, что большинство программ есть нечто большее, чем один-единственный класс и пара методов; для разумения принципов работы большинства методов недостаточно поверхностного взгляда. Даже если они отлично закомментированы (хе-хе!), мозгам придётся: во-первых, "сохранить информацию на диске", во-вторых, "включить поиск", затем - "пропарсить и сохранить на диск". И уже после всего этого - "найти необходимое место". Если ваше "сохранить на диск" подобно моему, то, к тому времени, когда дойдёте до конца логической цепочки, напрочь забудете, для чего переместили кнопку туда, где она находится сейчас. Как бы ни была великолепна фотографическая память, ей не справиться с семью последовательными элементами за семь секунд. Следовательно, нужно распрощаться с именами, допускающими неоднозначное толкование. Раз и навсегда.

На самом деле предполагалось, что метод переместит кнопку в позицию с указанными координатами, а не будет их использовать для относительного смещения. Откуда я это знаю? Ха! Да потому что Я это написал. Теперь я перепишу это так, чтобы вы тоже знали. Выберем более очевидный термин "Location" и продолжим.

s1.Location( 20, 30 );

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

s1._x=20;
s1._y=30;

А вот это действительно хороший вопрос. На который есть простой, но чересчур длинный ответ. Что-то типа: "Ну и когда же мы доберёмся до этой главы?"


Getters и Setters

myButton._x=20, myButton._y=20;

Координаты в пикселах. Первая проблема, вы будете смеяться, что ежели позднее понадобится использовать эту кнопку на карте, координаты которой исчисляются широтой и долготой? (Смех за кадром.) Хорошо, а как насчёт игрушки с мозаичной структурой, где всё, вместо пикселов, измеряется отдельными частями (tile[x,y])? О, да, конечно, игра - это серьёзная проблема... Возможно, вы всё ещё в сомнениях, вероятно делали когда-то нечто подобное без проблем. В таком случае ситуация хуже, чем предполагалось. Ладно, а как вам такое: что будет, если, по непонятным причинам, понадобится поместить кнопку за пределами экрана? Разумеется, на черта вам это делать, а как насчёт того чувака, что сидит за вашим рабочим столом по понедельникам и вечно с бодуна? Не следует доверять лицам, использующим ваши классы, они всегда делают это не так, как нужно. Все они, как один, подлецы, извращенцы, злобные ублюдки и байстрюки. Сказать что-либо в их защиту крайне трудно, но объективности ради признаем, что напакостить они могут и случайно, к примеру, во вторник, после кофе, когда будут просто перемещать окошки... Так почему бы не обезопасить себя? Сделать это легко: заведите метод для изменения x, вместо проверок x на корректность всякий раз, как вы его меняете и заставьте его правильно работать.

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

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

Ещё одна приятная особенность состоит в том, что отныне вы не обязаны устанавливать непосредственные отношения между методами и данными. Вместо того чтобы говорить setX и setY, можно сказать проще: setLocation. Вместо того чтобы говорить setWidth и setHeight, мы можем сказать setSize. Разумеется, где-то внутри нашего класса существуют X, Y, Widht и Height, но пользователь не должен забивать себе этим голову. Или сделать иначе, для установки одних и тех же значений создать не один метод, а несколько. К примеру, метод setRectangle, который установит все 4 поля "одним залпом". Внутри он может вызвать оба метода setLocation и setSize, или те же setX, setY, setWidth, setHeight один за другим - ну вот, пользователи класса уже не теряются в догадках о происходящем (что является их обычным состоянием).

Неплохие аргументы за использование того, что зовётся "getters и setters"? Суть в том, что теперь класс обладает свойством, которым можно управлять только через методы класса. Исторически такие методы назывались getXX() и setXX(), отсюда и названия терминов: "getters и setters". Однако учтите, их следует использовать лишь в том случае, если вы не уверены, что данное свойство никогда не будет подтверждаться, ограничиваться, регламентироваться или обрабатываться как-то иначе, но точно знаете, что значение свойства никогда не будет преобразовано во что-то другое. Если же одолевает лень, попробуйте заставить себя сделать это несколько раз, и в будущем подобная практика станет естественной. Она может даже перерасти в привычку: поставьте себе цель делать это, после того как закомментируете код. Хм...

<сноска>
Погодите-ка, разве вышеупомянутый .NET-пример не использует "getters и setters"?

button1.Location = new Point (168, 168);

Это определенно не выглядит как "setter", не так ли? Ну, по правде говоря, это и не "setter", а то, что известно под именем "свойство" (Мд-а... ещё больше запутались!) в C#, и что автоматически вызывает метод get, когда значение читается, и метод set, когда значение устанавливается. "Майкрософту" так нравятся эти методы, что они встроили их в язык C# (на который обладают монополией), одним махом избавившись от необходимости когда-либо использовать методы getXX или setXX. Что ж, мы не имеем такой роскоши в ActionScript, поэтому придётся обзывать методы setXX и getXX вручную. Да, не так удобно, зато у них довольно посредственная поддержка векторной графики, вот!

Ладно, теперь, зная всё это, давайте попробуем заново создать класс Shape. На этот раз чуть меньше "псевдо" и чуть больше "кода".

// Shape Class
Shape = function()
{
    // тут ничего делать не надо
}
// Properties
x,y,width,height // давайте использовать настоящие имена, раз уж на то пошло.
// Methods
setLocation( x, y )
{
    this.x = x;
    this.y = y;
}
getLocation()
{
    return this.x;
    return this.y; // назревает проблемка? Хе-хе...
}
setSize( w, h )
{
    this.width = w;
    this.height = h;
}
getSize()
{
    return this.width;
    return this.height; // что же делать?
}

Не так уж много кода понадобилось написать, чтобы всплыла новая проблема: разумеется, с getLocation и getSize, ибо невозможно сделать возврат из метода дважды, а нам-то нужно вернуть два значения. Самое очевидное решение - запаковать оба значения в объект и вернуть объект, например: obj.x и obj.y. Это выход из положения, но как теперь сообщить пользователю, что его ожидает? Можно пойти на дополнительный шаг: вместо того, чтобы возвращать абстрактный объект, создадим стандартный объект, который будет использоваться всегда. Пусть это заставит кое-кого ответственнее относиться к работе, что ж, ответственность крайне важная черта, как для разработчиков, так и для продавцов софта.

Какова же основная единица измерения позиции? А это смотря какими параметрами измерять: x и y, шириной и долготой, xyz, или даже углом-радиусом, все они задают точку. Мы будем работать с 2D и использовать декартовы координаты, поэтому наша точка будет содержать x и y. Как вы уже наверно догадались, мы зададим точку как класс:

// Point Class
Point= function( x, y )
{
    this.x = x;
    this.y = y;
}

Теперь, чтобы задать позицию, можно сказать:

s1.setLocation( new Point(20, 30) );

Помимо решения проблемы с get, мы получили ещё ряд преимуществ. Во-первых, наш код стал гораздо чище. Достаточно только взглянуть на одну строку, чтобы понять, что позиция s1 устанавливается в определённую точку. Кроме того, метод setLocation теперь точно знает, что он получит, объект Point не такой уж и большой, но если нужно использовать более сложные объекты, очень пригодится знание того, что все значения установятся в дефолтные, если не будут заданы явно. Да и копирование свойств одного объекта в другой теперь становится проще:

s2.setLocation( s1.getLocation );

Конечно, это можно было бы сделать двумя командами, но что за нужда вникать в детали? Представьте, что копируете Rectangle, а не Location, вот вам уже и четыре команды. То ли дело, используя методы get и set State, можно копировать полное состояние чего-нибудь, например целого мувиклипа (x, y, rotation, alpha, ...)

Как видите, несколько выгодных позиций у нас уже есть, но избавляет ли это нас от необходимости использовать setX и setY? Если Location хранится в объекте Point, как тогда прямоугольник сохраняется в объекте Rectangle, здесь что, две версии? Суть в том, что всё по-прежнему сводится к x, y, ширине и высоте. Когда мы устанавливаем позицию, то вытаскиваем x и y из объекта Point и присваиваем их значения свойствам x и y. А ширина и высота, поступившие из объекта Size, присваиваются свойствам width и height. Объект Rectangle, устанавливающий все четыре свойства, может либо проделать это через объекты Location и Size, либо установить все свойства сам. И, конечно, всё ещё можно использовать методы setX и setY. Работает это примерно так:

// Методы
setX( x )
{
    this.x = x;
}
getX( )
{
    return this.x;
}
setLocation( newLocation )
{
    this.x = newLocation.x;
    this.y = newLocation.y;
}
getLocation()
{
    return new Point( this.x, this.y );
}
setRectangle( newRectangle )
{
    this.x = newRectangle.x;
    this.y = newRectangle.y;
    this.width = newRectangle.width;
    this.height = newRectangle.height;
}
getRectangle()
{
    return new Rectangle(
        this.x,
        this.y,
        this.width,
        this.height );
}

Несмотря на то, что этот код достаточно условный, он абсолютно реален. Или, возможно, реален "c некоторыми если".

<fla>
< Тут, наверное, должен был прилагаться какой-то исходник, но автор видимо постеснялся... ;)
(прим. переводчика) >

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

А теперь о подклассах.


Настоящий Подкласс (Subclass)

Если бы подкласс существовал как класс информатики, мы бы могли уже сейчас создать подклассы Circle (Окружность) и Square (Квадрат), оба они содержали бы метод getArea (вычислить площадь) и каждый из них возвращал бы площадь, вычисленную абсолютно разными способами. На самом деле это изящная, но отчасти бесполезная демонстрация того, как OOП позволяет использовать один и тот же интерфейс для разных объектов. Важно другое: вы уже не видите "внутренних" калькуляций, а просто верите каждому, кто выдаёт правильный размер площади.

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

Идея инкапсуляции, или сокрытия неактуальной для пользователя информации, должна всегда быть у вас на уме при проектировании классов. Сообщать пользователю следует лишь то, что ему нужно знать, и ничего из того, что ему знать не нужно. Более того, пользователю совершенно незачем быть в курсе всех деталей работы класса (типа способа вычисления площади) перед его использованием. Всё, что ему нужно, уверенность в том, что значение площади возвращается, когда он вызывает метод getArea, независимо от типа геометрической фигуры.

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

Теперь мы, как и собирались, создадим два подкласса, Circle (круг) и Square (квадрат). Но что же делает их настолько отличными от Shape (фигуры) и друг от друга, что потребовались отдельные подклассы? Наиболее очевидный ответ: они по-разному выглядят. Верно, но на этом вся сила данного аргумента и исчерпывается. В конце концов, красный квадрат отличается от синего квадрата, а круглая маска похожа на всё, что находится под ней. Фактически, пока наша форма не похожа ни на что, классы Circle и Square, скорее всего, также обречены на "бесформенность". Ведь можно же представить такой класс Circle, который работает только с формулами. Хотя, скорее всего, вы вовсе ни в чём не уверены и вам кажется, что класс Circle, в принципе, заведомо обязан быть похож на круг. А что если мы заведём другой подкласс класса Shape, класс Polygon (многоугольник), например? Можно ли по изображению многоугольника судить о его идее? Как выглядит "многоугольник", ну-ка? Разумеется, многоугольник не обязан быть визуально "похожим" на что-то, он просто должен быть чётко выражен математическими терминами, находиться в пределах неких "математических ограничений". А уж компьютеры-то как это любят!

Итак, нужно задать "математические условия" для кругов и квадратов. Не волнуйтесь, ребята, это не сложнее задачи для пятиклашки! Всем известно, что круг задаётся через точку центра и радиус, а квадраты имеют длину и ширину. *Бабах!* С этим звуком реальность врезалась в нашу теорию. Неважно, программистом какой глубины мысли вы являетесь, но, вероятно, уже создавали во Flash как круг, так и квадрат. И знаете, что у них есть позиции x и y, а также ширина и высота. Если любите думать в терминах X, Y, W, H, посмотрите на панель "Info". Эти значения есть даже у кругов. Возникает вопрос, а где радиус? Отвечаем: во Flash'е всякие там "радиусы-катеты-гипотенузы" не работают. И вы это должны бы уже знать. Круги находятся, так же как и квадраты, в условных рамках (bounding box), просто они не заполняют эту рамку по самые углы. У кругов есть и ширина и высота, а свойства X и Y представляют собой характеристики левого верхнего угла ограничивающей их рамки (точнее - точки привязки этой рамки, а такая точка может находиться и не в левом верхнем углу, а, например, в центре - прим. переводчика).

Здесь возникает еще один аспект реальности: как Круги, так и Квадраты имеют ширину и высоту, унаследованные от класса геометрических фигур. Вспомните учебник пятого класса: эти величины должны быть одинаковыми и для круга, и для квадрата. Вот вам и прекрасный шанс для использования OO, переопределите метод setWidth и одновременно с шириной установится высота, и наоборот. Метод setSize может быть перекрыт и второй параметр будет проигнорирован. Будет логично предположить, что Квадрат и Круг могут наследовать типовые характеристики от Прямоугольника и Эллипса, являясь их частными случаями (квадрат это тот же прямоугольник, если помните). Что ж, чудесно. Теперь можно и выпить. Алкоголь избавит нас от подсознательного ощущения, что здесь что-то не так... Такое вот "мастерское" использование перекрытых методов кажется вполне достаточным. Но это только кажется. В методах скрывается гораздо большее.

А для чего, собственно, снова задавать подкласс фигуры? Мы хотим сделать небольшую программку, которая создаёт фигуры и позволяет перетаскивать их туда-сюда. Несомненно, нам хочется разных типов форм, но в качестве отправной точки круги и квадраты выглядят вполне логично. И снова, если присмотреться, а не являются ли для Flash круги и квадраты одним и тем же? То бишь мувиклипами с графическими данными внутри? Копнём глубже, так ли уж нам важно наличие радиуса у круга? Нужно ли нам вообще это знать? Ведь не для разрисовки же пиксел за пикселом, в самом деле?! Может, это нужно для hitTesting'а, но для этого есть кнопки, свойство hitTest или даже какой-нибудь dropTarget для красоты... Не будем выделять что-то одно, но есть ли что-нибудь ещё?

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

shape1 = new Polygon ();
shape1.Location = new Point (168, 168);
shape1.points = {
    new Point(0, 0),
    new Point(50, 30),
    new Point(30, 60)
};

shape2 = new Circle ();
shape2.Location = new Point(168, 168);
shape2.Size = new Size (176, 56);

Итак, мы позволили пользователям нашего класса отвечать на поставленный вопрос. И даже если класс круга не делает ничего отличного от того, что делает класс квадрата (кроме отображения круглого мувиклипа), пользователь уже думает о них, как о разных вещах, так что отдельный класс всё-таки нужен. Конечно, наш горе-пользователь "представляет в уме" эллипс, называя его кругом (Глумливо так: "хе-хе"). Фактически, сразу после использования круга, он растянет его, чтобы превратить круг в эллипс! Да, перед нами дилемма. Использовать ли нам неправильные названия, пособничая тому, что пользователи классов называют эллипсы кругами, прямоугольники квадратами, а магнитофоны флейтами? Или (высокой цели ради), пойти трудной дорогой и использовать правильные термины, примирившись с тем, что пользователи проклинают нас каждый раз, когда используют "круг" в смысле "эллипс", да и просто когда не могут написать "эллипс" без ошибок? Разумеется, мы выберем "высокую цель". Ведь если мы назовем эллипс кругом, это введёт в заблуждение всех, кто до этого думал правильно, и они подумают, что более не смогут использовать "круг" как "эллипс". Кроме того, за правильное использование слова никого ещё не расстреляли. Да!.. и постарайтесь не давать название "Растрата" классу своих финансовых приложений.

А пока создадим-ка два подкласса Фигур: Эллипс и Прямоугольник. Потому, что это делается легко. Нам кажется, по крайней мере сейчас, что они не будут делать ничего, кроме создания формы и запоминания своих собственных имён (мы намеренно не касаемся пока работы с мувиклипами, как вы, наверное, заметили).

Ellipse = function(){}
Object.extends(Shape, Ellipse);
Rectangle = function(){}
Object.extends(Shape, Rectangle);

Пока мы не начали заниматься многоугольниками, посмотрите, есть ли проблемы в вышеприведённом коде? Сам код выглядит хорошо (кроме ужасного синтаксиса flash 5, который использован для создания наследования!), но не звучит ли "Прямоугольник" слишком знакомо? Не задавали ли мы также для объекта все его xywh, используя прежде прямоугольник? Да, именно так мы и делали, или, по крайней мере, говорили об этом. Класс Rectangle уже есть, он совершенно не связан с новым классом. Это проблема, потому что один из них перезапишется поверх другого (_root.Rectangle и _root.Rectangle!). И слава Богу, что мы обнаружили эту проблему сейчас, а не после написания километров кода. Следующий раздел - о Пространствах имён и он касается (ну надо же!) именно этой проблемы. Какое совпадение!


Пространства имён

Классы это, конечно, всего лишь рецепты по созданию объектов. Объект - торт со взбитыми сливками, а класс - рецепт по его приготовлению. Любимые рецепты можно записывать на карточках, которые, в свою очередь, можно хранить в коробочке (пока ещё компьютерная революция не добралась до кухни!). Для торта со взбитыми сливками у вас есть только один рецепт (допустим), а вот для шоколадного их уже несколько. Один любимый рецепт найти легко, так как он обычно покрыт засохшим шоколадным тестом, а если рецептов двенадцать? Все они соответствуют шоколадному торту и, даже просматривая ингредиенты (проверяя классы), не так-то легко их отсортировать. Вероятнее всего, поначалу вы будете "помечать" их с помощью дополнительной информации. "Бабушкин шоколадный торт", "Юлин шоколадный торт для детей", "Шоколадный торт без сахара"... Вот простейшее объяснение использования пространств имён - чтобы как-то отделять друг от друга вещи с одинаковыми именами. Однако, как любит говорить ваша бабушка, за каждой лишней заморочкой кроется возможность структурирования библиотек класса способом, помогающим не загрязнять пространства имён и облегчающим многократное использование кода. Да хранит её Господь, вашу бабушку!

Взглянем на "проблему" с прямоугольником ещё раз, имеется один класс, "математически" описывающий размеры прямоугольника (x, y, w, h) от некоей точки в пространстве. И другой, описывающий объект "Прямоугольник" как форму (например, он может быть синим), в противоположность формам эллипса или треугольника. Итак, перед нами два, чётко отличимых друг от друга способа описания объекта, и если они будут одинаково называться, проблем не избежать. Мы могли бы просто переименовать один из них, но, увы, это не всегда возможно, что если используются чужие классы, или же два собственных класса из предыдущих проектов? Что, если у нас перед глазами спецификация, в которой чётко указано: rectangle? Ещё хуже, когда мы не замечаем, что происходит, а заметив, не можем понять, почему порой всё перемешивается. При использовании пространства имён решаются не только эти проблемы. Классы теперь можно организовать в стройную логическую структуру, в которой поиск данных и их использование (повторное в том числе) заметно облегчены. "Почему же не все и не всегда используют пространства имён?", - спросите вы. В большинстве случаев всё-таки используют.

Короче говоря, пространства имён упаковывают группу связанных классов в некий "ящичек". Возвращаясь к аналогии с рецептами, можно сказать, что у нас появились как бы разные ящички для разных групп рецептов и этих ящичков на одни только рецепты шоколадных тортов будет много! Один для бабушкиного, один для Юлиного... Ваши догадки верны: ящички - по сути объекты, а одни ящички могут содержать в себе другие ящички. Так что, решив однажды приготовить вкуснятины "по бабушкиным рецептам", мы берём её ящичек, открываем и находим всякую всячину. Там и ящички "кондитерские изделия", и ящички "паштеты" и ящички "пудинги"... Пространства имён то же самое. Вместо того чтобы задавать классы в _root или _level0, задавайте их внутри каждого отдельного объекта (к примеру, не _level0.ChocolateCake, а _level0.Grandma.Cake.ChocolateCake). Когда задаются эти "пространства имён" на главной линейке, в нём может одновременно присутствовать много чего: приаттаченные клипы, загружаемые клипы, и даже случайно задаваемые свойства (когда вы забываете использовать var). Так что основной линейкой сам по себе ничего не гарантирует. Легко убедиться, что при загрузке .swf-файла одновременно не перезаписывается _level0, однако запомните, если хотите, чтобы ваш класс использовали другие люди, задание пространств имён просто необходимо. Причём пользователи должны знать об этом. "Безопасное" место для хранения, это пространство имён внутри Object, потому что последний не связан с линейкой.

Давайте на мгновение оставим воображаемый мир бабушкиной кухни и вернёмся к реальности определения классов в пространствах имён. Обычно выбирается ваше собственное пространство имён верхнего уровня, где находятся определения всех классов. Если вы работаете в компании, то её название неплохой вариант для обозначения пространства имён (разумеется, если компания не называется "Object.prototype"!). Если название по тем или иным причинам не подходит, тогда за имя можно взять бренд любимых ботинок, название причёски, название какого-нибудь еженедельника. Между прочим, можно использовать собственное имя. Не имеет значения, какое название вы используете, но так будет лишь до тех пор, пока им не пользуется никто другой. В Java для обозначения пространства имён обычно используют доменное имя компании, записанное "наоборот", сей способ, несомненно, уникален (кстати, в Java пространства имён называются "пакеты" (packages)). Что ж, это неплохой вариант, если вы доменные имена меняете нечасто, да не так их и много у вас, то используйте на здоровье. Следуя этой системе, можно, например, воспользоваться системой меню "Sparkysoft" и классами кнопок "Aquamedia", и при этом не попасть на два одинаковых определения в одном месте для классов типа "container". Кроме того, свежесозданные классы всегда будут "знать", где найти классы, написанные полгода назад.

Реализуя пространства имён на верхнем уровне, можно с лёгким сердцем делиться классами с остальным миром; однако "проблема Прямоугольника" (помните ещё?) пока не разрешена. Итак, теперь можно создавать категории для других типов реализуемой функциональности внутри нашего собственного пространства имён. "Сладкое" и "жидкое" - "Торт" и "Суп". Можете попробовать рассмотреть чужие пространства имён, чтобы понять, какими причинами руководствуются другие люди, но это уже будет наука на грани искусства. Вот некоторые примеры пространств имён (и "пакетов") для начала:

<get examples>
< и снова неподражаемая робость автора лишает нас возможности познакомиться с этими, безусловно полезными, примерами :) >

Итак, общие классы входят в общие пространства имён, подобно XML, или Data, или Drawing.Drawing2D... Эти встроенные классы часто обозначаются как принадлежащие к "Каркасу" ('Framework'). У Flash чрезвычайно скудный каркас, так что многое придётся сделать самостоятельно. Классы будут использоваться многократно, так что лучше уж прямо сейчас потрудитесь сделать их устойчивыми и универсальными. Они должны безотказно работать в различных ситуациях, а не в отдельных частных случаях. Затем, когда потребуется решить определённую задачу, не совсем ту, которая предоставляется общими классами "каркаса", надо создать подкласс и добавить требуемую функциональность в нём. В ActionScript "каркас" - это встроенные объекты типа класса Color, или Movieclip, короче, всё, что начинается с заглавной буквы в словаре ActionScript. Сравните с элементами языка for или return и увидите, каким образом "каркас" наделяет приложения "индивидуальностью". Порой сложно сказать, на C++ или на VisualBasic написана программа под Windows, а вот разница между программой на C++ для KDE (Linux) и программой на C++ для Macintosh выявляется легко. Дело в том, что мы (как правило) видим на дисплее приложения, созданные на основе встроенных классов, причём таких, которые априори используются всеми приложениями данной платформы. Так как во Flash почти нет таких вот встроенных классов, можно сэкономить кучу времени, если в процессе программирования мыслить с точки зрения заполнения "каркаса".

Вот теперь, пребывая в некоем абстрактном мире совершенства, кто-то мог подумать, что каждый класс должен быть достаточно чистым и благоухающим, чтобы принадлежать каркасу. Однако задача большинства классов в том, чтобы сделать первый толчок, достаточно мощный, и тихо отойти в тень. Это не означает, что классы должны лишь банально определяться в _root, а имя их может быть некрасивым, нет, они тоже должны быть защищены от конфликтов имён и от перезаписи каким-нибудь подгруженным клипом. Итак, эти классы также войдут в пространство имён и, в конце концов, также будут заданы в пространстве имён Object, просто они будут как бы отделены от каркаса. Фактически, даже экземпляры, в конце концов, могут быть созданы в пространстве имён Object, освобождая линейку от всего, кроме мувиклипов.

Конечно, в "обычном", а не Flash-программировании, проблем с клипами, подгружаемыми в пространство, где пишется код, не возникает, поскольку там нет линейки, весь код автоматически находится в "безопасном" месте. Во Flash же постоянно приходится иметь дело с двумя "областями видимости": в объекте и в линейке. Вот почему во Flash так часто встречается слово "this", оно означает "в этом объекте", а не на линейке, как принято по умолчанию. Поэтому-то мы можем определять код как принадлежащий пространству имен Object, даже если всё должно быть прикреплено к линейке. Если сомнения в пользе вышеописанного всё ещё остаются, представьте себе загрузку класса из внешнего клипа. Если класс определяется в _root клипа, то используемый для создания экземпляров класса код будет зависеть от имени клипа и пути к нему, и будет разным чуть ли не каждый новый раз. Тем не менее, если загруженный клип дублирует определение класса где-то в пространстве имён Object, можно создавать его экземпляры каждый раз одним и тем же способом, облегчая будущее многократное использование (а также отладку!). Разумеется, вы будете уверены, что, сколько клипов ни загрузить таким образом, конфликтов имён не будет. Так, а какие же новости у нас плохие?

Плохие новости, вероятно, можно выразить в трёх словах, появится "синдром усталого запястья". Если вам не знаком секрет машинописного набора длинных префиксов классов, то скоро обнаружите, что набивать на клавиатуре длиннющие маршруты к классам в ActionScript, дело настолько тяжёлое... Вот я заканчиваю писать, а руки мои уже отваливаются.

На этом до свидания!


О проекте

Авторские права на учебник принадлежат его создателю
Робину Дебройлу (Robin Debreuil).

Первоисточник на английском: http://www.debreuil.com/docs/

ДАННАЯ РЕДАКЦИЯ УЧЕБНИКА ПРЕДНАЗНАЧЕНА ИСКЛЮЧИТЕЛЬНО ДЛЯ ЛИЧНОГО OFF-LINE ПОЛЬЗОВАНИЯ, ЛЮБОЕ КОММЕРЧЕСКОЕ ИСПОЛЬЗОВАНИЕ И ПУБЛИКАЦИЯ В СЕТИ НЕДОПУСТИМЫ !

Переводчики:

  • Rost Siryk <rost@rs.com.ua>
  • Serg Pilguy <fix@rs.com.ua>
  • Viktor Viton <viton@peterlink.ru> [ общая редакция ]

Оформление:

  • VM Studio

18.03.2003

ruFlash NewsLetter Мейл-Конференция "ruFlash" ("RUSSIAN FLASH NEWS")
существует с мая 1999 года и предназначена для оперативного обмена новостями, мнениями между русскоязычными пользователями пакета Macromedia Flash, обсуждения технологии работы в нем и других аналогичных или связанных с ним программах. Разрешается обсуждение вопросов близких к тематике конференции или каким либо образом влияющих на развитие и распространение Flash технологии. Подписчиком конференции может быть любой, без каких либо ограничений, "житель" Сети, имеющий адрес электронной почты, если он согласен с ее Правилами и соблюдает их при общении с другими подписчиками.


Навигация

ООП во FLASH 5 ( II )
Вступление
Процесс
Настоящий Класс
Getters и Setters
Настоящий Подкласс
Пространства имён
О проекте

RuFlash NewsLetter

Флэш Потрошитель

© 2002
Debreuil Digital Works


ООП во Flash 5 ( II )

OOP in Flash5

Часть II

Автор: Robin Debreuil
Источник: www.debreuil.com/docs
© 2002 Debreuil Digital Works

Перевод сделан группой участников мейл-конференции
"ruFlash" и проекта "Флэш Потрошитель"