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