Pascal. Объект (OBJECT)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В Турбо Паскале для создания объектов используются три зарезервированных слова: 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; <i>{Ошибка!}</i>
  Color: Word;<i>{Ошибка!}</i>
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(Xl,Yl,aColor);
  {Инициируем поля dX и dY}
  dX := X2-X1;
  dY := Y2-Y1
end;
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);

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

TGraphObj.Init(XI,Y1,aColor);

Для инициации полей dX и dY вычисляется расстояние в пикселах по горизонтали и вертикали от первого конца прямой, до ее второго конца. Это позволяет в методе TLine.Draw вычислить координаты второго кони по координатам первого и смещениям dX и dY. В результате простое изменение координат реперной точки Х,У в родительском i 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);

Идею инкапсуляции полей и алгоритмов можно применить не только к графическим объектам, но и ко всей программе в целом. Ничто не мешает нам создать объект — программу и “научить” его трем основным действиям: инициации (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), весьма небольшая плата за мощный инструмент создания современного программного обеспечения.