Создание объектов

В Турбо Паскале для создания объектов используются три зарезервированных слова: object, constructor, destructor к три стандартные директивы: private, public и virtual.

Зарезервированное слово object используется для описания объекта. Описание объекта должно помещаться в разделе описания типов:

type

MyObject = object

(Поля объекта}

{Методы объекта} 

end ;

Если объект порождается от какого-либо родителя, имя родителя указывается в круглых скобках сразу за словом object:

type

MyDescendantObject = object(MyObject)

.

.

end;

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

Для нашей учебной задачи создадим объект-родитель TGraphObject, в рамках которого будут инкапсулированы поля и методы, общие для всех остальных объектов:

type

TGraphObj = object

Private {Поля объекта будут скрыты от пользователя}

X,Y: Integer; {Координаты реперной точки}

Color: Word; {Цвет фигуры} 

Public {Методы объекта будут доступны пользователю}

Constructor Init(aX,aY: Integer; aColor: Word);

{Создает экземпляр объекта}

Procedure Draw(aColor: Word); Virtual;

{Вычерчивает объект заданным цветом aColor}

Procedure Show;

{Показывает объект - вычерчивает его цветом Color}

Procedure Hide;

{Прячет объект - вычерчивает его цветом фона} 

Procedure MoveTo(dX,dY: Integer);

{Перемещает объект в точку с координатами X+dX и Y+dY}

end; {Конец описания объекта TGraphObj}

В дальнейшем предполагается создать объекты-потомки от TGraphObj, реализующие все специфические свойства точки, линии, окружности и прямоугольника. Каждый из этих графических объектов будет характеризоваться положением на экране (поля X и Y) и цветом (поле Color). С помощью метода Draw он будет способен отображать себя на экране, а с помощью свойств «показать себя» (метод Show) и «спрятать себя» (метод Hide) сможет перемещаться по экрану (метод MoveTo). Учитывая общность свойств графических объектов, мы объявляем абстрактный объект TGraphObj, который не связан с конкретной графической фигурой. Он объединяет в себе все общие поля и методы реальных фигур и будет служить родителем для других объектов.

Директива Private в описании объекта открывает секцию описания скрытых полей и методов. Перечисленные в этой секции элементы объекта «не видны» программисту, если этот объект он получил в рамках библиотечного ТР(/-модуля. Скрываются обычно те поля и методы, к которым программист (в его же интересах!) не должен иметь непосредственного доступа. В нашем примере он не может произвольно менять координаты реперной точки (X.Y), т.к. это не приведет к перемещению объекта. Для изменения полей X и Y предусмотрены входящие в состав объекта методы Init и MoveTo. Скрытые поля и методы доступны в рамках той программной единицы (программы или модуля), где описан соответствующий объект. В дальнейшем предполагается, что программа будет использовать модуль GraphObj с описанием объектов. Скрытые поля будут доступны в модуле GraphObj, но недоступны в использующей его основной программе. Разумеется, в рамках реальной задачи создание скрытых элементов объекта вовсе необязательно. Я ввел их в объект TGraphObj лишь для иллюстрации возможностей ООП.

Директива public отменяет действие директивы private, поэтому все следующие за public элементы объекта доступны в любой программной единице. Директивы private и public могут произвольным образом чередоваться в пределах одного объекта.

Вариант объявления объекта TGraphObj без использования механизма private...public:

type

TGraphObj = object

X,Y: Integer;

Color: Word;

Constructor Init(aX,aY: Integer; aColor: Word);

Procedure Draw(aColor: Word); Virtual;

Procedure Show;

Procedure Hide;

Procedure MoveTo(dX,dY: Integer); 

end;

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

В нашем примере она совпадает с координатами точки в описываемом ниже объекте TPoint, с центром окружности в объекте TCircle, первым концом прямой в объекте TLine и с левым верхним углом прямоугольника в объекте TRect.

Для описания методов в ООП используются традиционные для Паскаля процедуры и функции, а также особый вид процедур - конструкторы и деструкторы. Конструкторы предназначены для создания конкретного экземпляра объекта, ведь объект - это тип данных, т.е. «шаблон», по которому можно создать сколько угодно рабочих экземпляров данных объектного типа (типа TGraphOhj, например). Зарезервированное слово constructor, используемое в заголовке конструктора вместо procedure, предписывает компилятору создать особый код пролога, с помощью которого настраивается так называемая таблица виртуальных методов (см. ниже). Если в объекте нет виртуальных методов, в нем может не быть ни одного конструктора, наоборот, если хотя бы один метод описан как виртуальный (с последующим словом Virtual, см. метод Draw), в состав объекта должен входить хотя бы один конструктор и обращение к конструктору должно предшествовать обращению к любому виртуальному методу.

Типичное действие, реализуемое конструктором, состоит в наполнении объектных полей конкретными значениями. Следует заметить, что разные экземпляры одного и того же объекта отличаются друг от друга только содержимым объектных полей, в то время как каждый из них использует одни и те же объектные методы. В нашем примере конструктор Init объекта TGraphObj получает все необходимые для полного определения экземпляра данные через параметры обращения аХ, аY и aColor.

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

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

Наличие в объекте TGraphObj виртуального метода Draw позволяет легко реализовать три других метода объекта: чтобы показать объект на экране в методе Show, вызывается Draw с цветом aColor, равным значению поля Color, а чтобы спрятать графический объект, в методе Hide вызывается Draw со значением цвета GetBkColor, т.е. с текущим цветом фона.

Рассмотрим реализацию перемещения объекта. Если потомок TGraphObj (например, TLine) хочет переместить себя на экране, он обращается к родительскому методу MoveTo. В этом методе сначала с помощью Hide объект стирается с экрана, а затем с помощью Show показывается в другом месте. Для реализации своих действий и Hide, и Show обращаются к виртуальному методу Draw. Поскольку вызов MoveTo происходит в рамках объекта TLine, используется ТВМ этого объекта и вызывается его метод Draw, вычерчивающий прямую. Если бы перемешалась окружность, ТВМ содержала бы адрес метода Draw объекта TCircle и визуализация-стирание объекта осуществлялась бы с помощью этого метода.

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

type 

TGraphObj = object

...

end;

Constructor TGraphObj.Init;

begin

X := aX;

Y := aY; Color := aColor 

end;

Procedure TGraphObj-Draw; 

begin

{Эта процедура в родительском объекте ничего не делает, поэтому экземпляры TGraphObj не способны отображать себя на экране. Чтобы потомки объекта TGraphObj были способны отображать себя, они должны перекрывать этот метод} 

end;

Procedure TGraphObj.Show; 

begin

Draw(Color) 

end;

Procedure TGraphObj.Hide; 

begin

Draw(GetBkColor) 

end;

Procedure TGraphObj.MoveTo; 

begin

Hide;

X := X+dX;

Y := Y+dY;

Show 

end;

Отмечу два обстоятельства. Во-первых, при описании методов имя метода дополняется спереди именем объекта, т.е. используется составное имя метода. Это необходимо по той простой причине, что в иерархии родственных объектов любой из методов может быть перекрыт в потомках. Составные имена четко указывают принадлежность конкретной процедуры. Во-вторых, в любом объектном методе можно использовать инкапсулированные поля объекта почти так, как если бы они были определены в качестве глобальных переменных. Например, в конструкторе TGraph.Init переменные в левых частях операторов присваивания представляют собой объектные поля и не должны заново описываться в процедуре. Более того, описание

Constructor TGraphObj.Init; 

var

X,Y: Integer; {Ошибка!} 

Color: Word; {Ошибка!} 

begin

end;

вызовет сообщение о двойном определении переменных X, Y и Color (в этом и состоит отличие в использовании полей от глобальных переменных: глобальные переменные можно переопределять в процедурах, в то время как объектные поля переопределять нельзя).

Обратите внимание: абстрактный объект TGraphObj не предназначен для вывода на экран, поэтому его метод Draw ничего не делает. Однако методы Hide, Show и MoveTo «знают» формат вызова этого метода и реализуют необходимые действия, обращаясь к реальным методам Draw своих будущих потомков через соответствующие ТВМ. Это и есть полиморфизм объектов.

Создадим простейшего потомка от TGraphObj - объект TPoint, с помощью которого будет визуализироваться и перемещаться точка. Все основные действия, необходимые для этого, уже есть в объекте TGraphObj, поэтому в объекте TPoint перекрывается единственный метод - Draw.

type

TPoint = object(TGraphObj)

Procedure Draw(aColor); Virtual; 

end;

Procedure TPoint.Draw; 

begin

PutPixel(X,Y,Color) {Показываем цветом Color пиксель с координатами X и Y} 

end;

В новом объекте TPoint можно использовать любые методы объекта-родителя TGraphObj. Например, вызвать метод MoveTo, чтобы переместить изображение точки на новое место. В этом случае родительский метод TGraphObj.MoveTo будет обращаться к методу TPoint.Draw, чтобы спрятать и затем показать изображение точки. Такой вызов станет доступен после обращения к конструктору Init объекта TPoint, который нужным образом настроит ТВМ объекта. Если вызвать TPoint.Draw до вызова Init, его ТВМ не будет содержать правильного адреса и программа «зависнет».

Чтобы создать объект-линию, необходимо ввести два новых поля для хранения координат второго конца. Дополнительные поля требуется наполнить конкретными значениями, поэтому нужно перекрыть конструктор родительского объекта:

type

TLine = object(TGraphObj)

dX,dY: Integer; {Приращения координат второго конца} 

Constructor Init(X1,Y1,X2,Y2: Integer; aColor: Word); 

Procedure Draw(aColor: Word); Virtual;

end; ,

Constructor TLine.Init;

{Вызывает унаследованный конструктор TGraphObj для инициации полей X, Y и Color. Затем инициирует поля dX и dY} 

begin

{Вызываем унаследованный конструктор}

Inherited Init(XI,Yl,aColor);

{Инициируем поля dX и dY}

dX := Х2-Х1;

dY := Y2-Y1 

end;

Procedure Draw; 

begin

SetColor(Color);{Устанавливаем цвет Color} 

Line(X,Y,X+dX,Y+dY){Вычерчиваем линию}

end;

В конструкторе TLine.Init для инициации полей X, Y и Color, унаследованных от родительского объекта, вызывается унаследованный конструктор TGraph.Init, для чего используется зарезервированное слово inherited (англ.- унаследованный):

Inherited Init(XI,Yl,aColor) ;

С таким же успехом мы могли бы использовать и составное имя метода:

TGraphObj.Init(Xl,Yl,aColor);

Для инициации полей dX и dY вычисляется расстояние в пикселах по горизонтали и вертикали от первого конца прямой до ее второго конца. Это позволяет в методе TLine.Draw вычислить координаты второго конца по координатам первого и смещениям dX и dY. В результате простое изменение координат реперной точки X, Y в родительском методе TGraph.MoveTo перемещает всю фигуру по экрану.

Теперь нетрудно реализовать объект TCircle для создания и перемещения окружности:

type

TCircle = object(TGraphObj)

R: Integer; {Радиус}

Constructor Init(aX,aY,aR: Integer;

Procedure Draw(aColor: Virtual); 

end ;

Constructor TCircle.Init; 

begin

Inherited Init(aX,aY,aColor);

R := aR 

end ;

aColor: Word)

Procedure TCircle.Draw; 

begin

SetColor(aColor); {Устанавливаем цвет Color} 

Circle(X,Y,R) {Вычерчиваем окружность}

end;

В объекте TRect, с помощью которого создается и перемещается прямоугольник, учтем то обстоятельство, что для задания прямоугольника требуется указать четыре целочисленных параметра, т.е. столько же, сколько для задания линии. Поэтому объект TRect удобнее породить не от TGraphObj, а от TLine, чтобы использовать его конструктор Init:

type

TRect = object(TLine)

Procedure Draw(aColor: Word); 

end;

Procedure TRect.Draw; 

begin

SetColor(aColor);

Rectangle(X,Y,X+dX,Y+dY) {Вычерчиваем прямоугольник} 

end;

Чтобы описания графических объектов не мешали созданию основной программы, оформим эти описания в отдельном модуле GraphObj:

Unit GraphObj; Interface

{Интерфейсная часть модуля содержит только объявления объектов}

type

TGraphObj = object

...

end;

TPoint = object(TGraphObj)

...

end;

TLine = object(TGraphObj)

...

end;

TCircle = object(TGraphObj)

end;

TRect = object(TLine)

...

end; 

Implementation

{Исполняемая часть содержит описания всех объектных методов}

Uses Graph;

Constructor TGraphObj.Init;

...

end.

В интерфейсной части модуля приводятся лишь объявления объектов, подобно тому как описываются другие типы данных, объявляемые в модуле доступными для внешних программных единиц. Расшифровка объектных методов помещается в исполняемую часть implementation, как если бы это были описания обычных интерфейсных процедур и функций. При описании методов можно опускать повторное описание в заголовке параметров вызова. Если они все же повторяются, они должны в точности соответствовать ранее объявленным параметрам в описании объекта. Например, заголовок конструктора TGraphObj.Init может быть таким:

Constructor TGraphObj.Init; 

или таким:

Constructor TGraphObj.Init(aX,aY: Integer; aColor: Word);