Объекты

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

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

По мере прогресса в области вычислительной математики акцент в программировании стал смещаться с процедур в сторону организации данных. Оказалось, что эффективная разработка сложных программ нуждается в действенных способах контроля правильности использования данных. Контроль должен осуществляться как на стадии компиляции, так и при прогоне программ, в противном случае, как показала практика, резко возрастают трудности создания крупных программных проектов. Отчетливое осознание этой проблемы привело к созданию Алгола—60, а позже — Паскаля, Модулы—2, Си и множества других языков программирования, имеющих более или менее развитые структуры типов данных.

Начиная с языка Симула—67, в программировании наметился новый подход, который получил название объектно—ориентированного программирования (ООП). Его руководящая идея заключается в стремлении связать данные с обрабатывающими эти данные процедурами в единое целое — объект. Характерной чертой объектов является инкапсуляция (объединение) данных и алгоритмов их обработки, в результате чего и данные, и процедуры во многом теряют самостоятельное значение. Фактически объектно—ориентированное программирование можно рассматривать как модульное программирование нового уровня, когда вместо во многом случайного, механического объединения процедур и данных акцент делается на их смысловую связь.

Какими мощными средствами располагает объектно—ориентированное программирование наглядно демонстрирует библиотека Turbo Vision, входящая в комплект поставки Турбо Паскаля.

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

1. ОСНОВНЫЕ ПРИНЦИПЫ ООП

Объектно—ориентированное программирование основано на китах” — трех важнейших принципах, придающих объектам новые свойства. Этими принципами являются инкапсуляция, наследование, полиморфизм.

Инкапсуляция
Инкапсуляция есть объединение в единое целое данных и алгоритмов обработки этих данных. В рамках ООП данные называются полями объекта, а алгоритмы — объектными методами.

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

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

Наследование
Наследование есть свойство объектов порождать своих потомков. Объект — потомок автоматически наследует от родителя все поля и методы, может дополнять объекты новыми полями и заменять (перекрывать) методы родителя или дополнять их.

Принцип наследования решает проблему модификации свойств объекта и придает ООП в целом исключительную гибкость. При работе с объектами программист обычно подбирает объект, наиболее близкий по своим свойствам для решения конкретной задачи, и создает одного или нескольких потомков от него, которые “умеют” делать то, что не реализовано в родителе.

Последовательное проведение в жизнь принципа “наследуй и изменяй” хорошо согласуется с поэтапным подходом к разработке крупных программных проектов и во многом стимулирует такой подход.

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

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

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

2. ПОСТАНОВКА УЧЕБНОЙ ЗАДАЧИ

Знакомство с техникой ООП начнем на примере следующей учебной задачи. Требуется разработать программу, которая создает на экране ряд графических изображений (точки, окружность, линия, квадрат) и может перемещать эти изображения по экрану. Для перемещения изображений в программе будут использоваться клавиши управления курсором, клавиши Ноте, End, PgUp, PgDn (для перемещения по диагональным направлениям) и клавиша Tab для выбора перемещаемого объекта. Выход из программы — клавиша Esc.Техническая реализация программы потребует использования средств двух стандартных библиотек — CRT и GRAPH.

3 СОЗДАНИЕ ОБЪЕКТОВ

В Турбо Паскале для создания объектов используются три зарезервированных слова: 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 и У+dY} end; {Конец описания объекта TGraphObj}

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

Директива Private в описании объекта открывает секцию описания скрытых полей и методов. Перечисленные в этой секции элементы объекта “не видны” программисту. В нашем примере он не может произвольно менять координаты реперной точки [X,Y), т.к. это не приведет к перемещению объекта. Для изменения полей Х и Y предусмотрены входящие в состав объекта методы Init и MoveTo. Скрытые поля и методы доступны в рамках той программной единицы (программы или модуля), где описан соответствующий объект.

Директива 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;

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

Для описания методов в ООП используются традиционные для Паскаля процедуры и функции, а также особый вид процедур — конструкторы и деструкторы. Конструкторы предназначены для создания конкретного экземпляра объекта, ведь объект — это тип данных, т.е. “шаблон”, по которому можно создать сколько угодно рабочих экземпляров данного объектного типа (типа TGraphObj, например).

Зарезервированное слово constructor, используемое в заголовке конструктора вместо procedure, предписывает компилятору создать особый код пролога, с помощью которого настраивается так называемая таблица виртуальных методов. Если в объекте нет виртуальных методов, в нем может не быть ни одного конструктора, наоборот, если хотя бы один метод описан каквиртуальный (с последующим словом Virtual, см. метод Draw), в состав объекта должен входить хотя бы один конструктор и обращение к конструктору должно предшествовать обращению к любому виртуальному методу.

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

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

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

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

Рассмотрим реализацию перемещения объекта. Если .потомок TGraphObj Например, TLine) хочет переместить себя на экране, он обращается к родительскому методу MoveTo. В этом методе сначала с помощью Hide объект стирается с экрана, а затем с помощью Show показывается в другом месте.

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

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;

Отметим два обстоятельства. Во-первых, при описании методов имя метода дополняется спереди именем объекта, т.е. используется составное имя метода. Это необходимо по той простой причине, что в иерархий родственных объектов любой из методов может быть перекрыт в потомках. Составные имена четко указывают . принадлежность конкретной процедуры. Во-вторых, в любом объектном методе можно использовать инкапсулированные поля объекта почти так, как если бы они были определены в качестве глобальных переменных. Например, в конструкторе fGraph.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);

ProcedureDraw(aColor: Word); Virtual

end;

                    Constructor TLine.Init;

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

begin

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

Inherited Init(Xl,Yl,aColor);

{Инициируем поля dX и dY}dX := X2-X1;dY := Y2-Y1end;

Procedure Draw;

begin

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

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

end;

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

Inherited Init(Xl,Yl,aColor);

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

TGraphOb:).Init(XI,Y1,aColor);

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

Создание объекта «Круг»

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

type

TCircle = object(TGraphObj)

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

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

Procedure Draw(aColor: Virtual);

end;

Constructor TCircle.Init;

begin

Inherited Init(aX,aY,aColor) ;

R := aR

end;

procedure TCircle.Draw;

begin

SetCoior(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

SetCoior(aColor) ;

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

end;

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

Unit GraphObj;Interface

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

TGraphObj = object.end;

TPoint = object(TGraphOb3).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);

4. ИСПОЛЬЗОВАНИЕ ОБЪЕКТОВ

Идею инкапсуляции полей и алгоритмов можно применить не только к графическим объектам, но и ко всей программе в целом. Ничто не мешает нам создать объект—программу и “научить” его трем основным действиям: инициации (Init), выполнению основной работы (Run) и завершению (Done). На этапе инициации экран переводится в графический режим работы и создаются и отображаются графические объекты (100 экземпляров TPoint и по одному экземпляру TLine, TCircle, TRect). На этапе Run осуществляется сканирование клавиатуры и перемещение графических объектов. Наконец, на этапе Done экран переводится в текстовый режим и завершается работа всей программы.

Назовем объект—программу именем TGrapbApp и разместим его в модуле CraphApp (пока не обращайте внимание на точки, скрывающие содержательную часть модуля — позднее будет представлен его полный текст):

Unit GraphApp;

Interface type

TGraphApp = object

Procedure Init;

Procedure Run;

Destructor Done;

end;

Implementation

Procedure TGraphApp.Init;

end;

end.

В этом случае основная программа будет предельно простой:

Program Graph_0bjects;

Uses GraphApp;

var

App: TGraphApp;

begin

App.Init;

App. Run ;

App.Done

end.

В ней мы создаем единственный экземпляр App объекта—программы TGrahpApp и обращаемся к трем его методам.

Создание экземпляра объекта ничуть не отличается от создания экземпляра переменной любого другого типа. Просто в разделе описания переменных мы указываем имя переменной и ее тип:

var

App: TGraphApp;

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

App.Init;

App.Run;

App.Done;

Ниже приводится возможный вариант модуля GraphApp для нашей учебной программы:

Unit GraphApp;

Interface

Uses GraphObj;

const

NPoints = 100; {Количество точек}

type

{Объект-программа}

TGraphApp = object

Points: array [1..NPoints] of TPoint; {Массив точек}

Line: TLine; {Линия}

Rect: TRect; {Прямоугольник}

Circ: TCircle; {Окружность}

ActiveObj: Integer; {Активный объект}

Procedure Init;

Procedure Run;

Procedure Done;

Procedure ShowAll;

Procedure MoveActiveObj(dX,dY: Integers;

end;

implementation

Uses Graph, CRT;

procedure TGraphApp.Init;

{Инициирует графический режим работы экрана. Создает и отображает NPoints экземпляров объекта TPoint, а также экземпляры объектов TLine, TCircle и TRect}

var

D, R, Err, k: Integer;

begin

{Инициируем графику}

D := Detect; {Режим автоматического определения типа графического адаптера)

InitGraph (D,R,’\tp\bgi’); {Инициируем графический режим. Текстовая строка должна задавать путь к каталогу с графическими драйверами}

Err := GraphResult; {Проверяем успех инициации графики}

if Err<>0 then

begin

GraphErrorMsg(Err);

Halt

end;

{Создаём точки}

for k := 1 to NPoints do

Points[k].Init(Random(GetMaxX),Random(GetMaxY),Random(15)+1);

{Создаем другие объекты}

Line.Init(GetMaxX div 3,GetMaxY div 3,2*GetMaxX div 3, 2*GetMaxY div 3, LightRed);

Circ.Init(GetMaxX div 2,GetMaxY div 2,GetMaxY div 5,White);

Rect.Init(2*GetMaxX div 5,2*GetMaxY div 5,3*GetMaxX div 5, 3*GetMaxY div 5, Yellow);

ShowAll; {Показываем все графические объекты}

ActiveObj := 1 {Первым перемещаем прямоугольник}

end; {TGraphApp.Init}

{___________________}

Procedure TGraphApp.Run ;

{Выбирает объект с помощью Tab и перемещает его по экрану}

var

Stop: Boolean; {Признак нажатия Esc}

const

D = 5; {Шаг смещения фигур}

begin

Stop := False;

{Цикл опроса клавиатуры}

repeat

case ReadKey of {Читаем код нажатой клавиши}

#27: Stop := True; {Нажата Esc}

#9: begin {Нажата Tab}

ActiveObj:= ActiveObj+1 ;

if ActiveObj>3 then ActiveObj := 3

end;

#0: case ReadKey of

#71 MoveActiveObj(-D,-D) {Влево и вверх}

#72 MoveActiveObj( 0,-D) {Вверх}

#73 MoveActiveObj( D,-D) {Вправо и вверх]

#75 MoveActiveObj(-D, 0) (Влево}

#77 MoveActiveObj( D, 0) {Вправо}

#79 MoveActiveObj(-D, D) {Влево и вниз]

#80 MoveActiveObj ( О, D) (Вниз)

#81 MoveActiveObj( D, D) (Вправо и вниз}end

end;

ShowAll;

Until Stop

end;{TGraphApp.Run}

{————————}

Destructor TGraphApp.Done;

{Закрывает графический режим}

begin

CloseGraph;

end;{TGraphApp.Done}

{————————}

Procedure TGraphApp.ShowAll;

{Показывает все графические объекты}

var

k: Integer;

begin

for k := 1 to NPoints do Points[k].Show;

Line.Show;

Rect.Show;

Circ.Show

end;

{————————}

Procedure TGraphApp.MoveActiveObj;

{Перемещает активный графический объект}

begin

case ActiveObj of

1: Rect.MoveTo(dX,dY);

2: Circ.MoveTo(dX,dY);

3: Line.MoveTo(dX,dY)

end

end;

end.

В реализации объекта TGraphApp используется деструктор Done. Следует иметь в виду, что в отличие от конструктора, осуществляющего настройку ТВМ, деструктор не связан с какими-то специфичными действиями: для компилятора слова destructor и procedure — синонимы. Введение в ООП деструкторов носит, в основном, стилистическую направленность — просто процедуру, разрушающую экземпляр объекта, принято называть деструктором. В реальной практике ООП с деструкторами обычно связывают процедуры, которые не только прекращают работу с объектом, но и освобождают выделенную для него динамическую память.

В заключении следует сказать, что формалистика ООП в рамках реализации этой технологии в Турбо Паскале предельно проста и лаконична. .Согласитесь, что введение лишь шести зарезервированных слов, из которых действительно необходимыми являются три (object, constructor и virtual), весьма небольшая плата за мощный инструмент создания современного программного обеспечения.