|
||||||||||||||||
Паскаль для новичков (часть 33)Средства объектно-ориентированного программированияАвтор: Владислав Демьянишин
Объектно-ориентированное программирование (ООП) основывается на этаких «трех китах» – трех важнейших принципах, коими являются инкапсуляция, наследование и полиморфизм.Инкапсуляция – это объединение данных и подпрограмм обработки этих данных в единую программную структуру (объект). В ООП данные называются полями (fields) объекта, а подпрограммы – объектными методами (methods).
Инкапсуляция обеспечивает в широкой степени изоляцию объекта от внешнего окружения и существенно повышает надежность разрабатываемых программ, поскольку локализованные в объекте подпрограммы обмениваются с программой сравнительно небольшими объемами данных, причем количество и тип этих данных обычно тщательно контролируются. И как следствие, замена или модификация методов и данных, инкапсулированных в объект, как правило, не влечет за собой плохо прослеживаемых последствий для программы в целом (и в целях повышения защищенности программ в ООП почти не используются глобальные переменные).
Еще одним немаловажным достоинством инкапсуляции является легкость и простота обмена объектами, переноса их из одной программы в другую. В какой-то мере ООП “побуждает” программистов к разработке библиотек объектов.
Объект – это структура данных, по виду напоминающая комбинированный тип (запись), но может содержать не только поля, но и методы (процедуры и функции).
Описание типа объекта выглядит почти так же как и описание составного типа, однако вместо служебного слова record используется служебное слово object:
type
TLine = object
Left, Top, Right, Bottom : integer;
Enabled : boolean;
end;
где Left, Top, Right, Bottom, Enabled – поля объекта. Такой объект может хранить, например, координаты Left, Top и Right, Bottom вершины отрезка на экране, и статус видимости Enabled на экране. Это пример простейшего объекта, и пока преимуществ над обычными записями не видно. В отличие от записи, объект может иметь подпрограммы, управляющие его поведением в соответствии со значениями его полей. Тогда можно объявить метод Create( Left, Top, Right, Bottom : integer ) для начальной инициализации объекта. При этом имена входных параметров подпрограммы Create совпадут с именами полей объекта, что может привести к ошибке компиляции “Error 4: Duplicate identifier (Left)” (продублирован идентификатор Left). Поэтому логичнее будет все имена полей объекта озаглавливать буквой F (от слова Field–поле) тогда объявление объекта будет выглядеть по-другому:
type
TLine = object
FLeft, FTop, FRight, FBottom : integer;
FColor : word;
FEnabled : boolean;
procedure Create( ALeft, ATop, ARight, ABottom : integer );
procedure SetColor( AColor : word );
procedure Enable;
procedure Disable;
procedure Move( ALeft, ATop : integer );
function Left : integer;
function Top : integer;
function Right : integer;
function Bottom : integer;
function Enabled : boolean;
end;
В объявлении типа объекта, как и в данном объявлении, заголовки методов указываются после объявления всех полей объекта. И раз уж в объявлении указаны заголовки методов объекта, то далее в листинге следует обязательно указать полное описание методов, при этом имя метода должно состоять из имени типа объекта и собственного имени метода, разделенных точкой (по аналогии с комбинированными типами):
procedure TLine.Create( ALeft, ATop, ARight, ABottom : integer );
begin
FLeft := ALeft;
FTop := ATop;
FRight := ARight;
FBottom := ABottom;
FColor := White;
FEnabled := false;
end;
procedure TLine.SetColor( AColor : word );
begin
FColor := AColor;
end;
procedure TLine.Enable;
begin
if FEnabled then exit;
FEnabled := true;
Graph.SetColor( FColor );
Line( FLeft, FTop, FRight, FBottom );
end;
procedure TLine.Disable;
begin
if not FEnabled then exit;
FEnabled := false;
Graph.SetColor( GetBkColor );
Line( FLeft, FTop, FRight, FBottom );
end;
procedure TLine.Move( ALeft, ATop : integer );
var dx, dy : integer;
begin
Disable;
dx := FRight – FLeft;
dy := FBottom – FTop;
FLeft := ALeft;
FTop := ATop;
FRight := FLeft + dx;
FBottom := FTop + dy;
Enable;
end;
function TLine.Left : integer;
begin
Left := FLeft;
end;
…
function TLine.Enabled : boolean;
begin
Enabled := FEnabled;
end;
Причем в каждом методе объекта считаются известными (видимыми) все поля этого объекта. Такое слияние информации об объекте (полей) и методов его поведения (функционирования, деятельности) в едином объектном типе позволяет создавать замкнутую, законченную среду для виртуализации прототипов практически любой сложности, начиная от простой линии на экране дисплея и обычного электронного регулировщика дорожного движения (светофора), до сложнейших молекулярных и галактических систем.
То, что было проделано выше, называется не самим объектом, а лишь объектным типом. Чтобы стало возможным применять свойства объекта, необходимо создать экземпляр объекта, объявив переменную объектного типа:
var
Line : TLine;
после чего можно обратиться (вызвать, выполнить) любой метод объекта, указав составное имя метода, составленное по аналогии с селектором записи, но необходимо помнить, что слева от точки всегда должна находиться переменная объектного типа, а справа –
имя метода или поля:
begin
Line.Create(100,100,200,200);
Line.Enable;
Line.Move(200,100);
end.
Если таких обращений к элементам объекта много, то как и для записей, для объектов допустимо применение оператора присоединения, тогда данный фрагмент кода будет выглядеть так:
begin
with Line do begin
Create(100,100,200,200);
Enable;
Move(200,100);
end;
end.
Следует заметить, что, не смотря на то, что к полям объекта можно обращаться так же непосредственно, как и к его методам, это желательно делать только внутри его методов, так как обращение к полю объекта напрямую из программы является отступлением от объектно-ориентированного стиля программирования. Поэтому следует осуществлять все манипуляции с полями объекта только через его методы. Следующий код эквивалентен вызову метода Line.Create(100,100,200,200), и не вызовет ошибки, но в дальнейшем лишит программу гибкости, так как реализация метода Create может претерпеть изменения, дополнения, а данный код будет работать все так же по старинке.
begin
with Line do begin
FLeft := 100;
FTop := 100;
FRight := 200;
FBottom := 200;
FColor := White;
FEnabled := false;
end;
end.
Поэтому логичнее вызывать метод, вместо изменения значений полей объекта непосредственно из программы. К тому же вызов метода выглядит короче, что повысит читабельность программы.
Используя идентификатор объектного типа в var-блоке можно объявить сколь угодно экземпляров объекта.
Из всех подпрограмм, описанных в листинге программы, и используемых ею модулях, в результативный код войдет код только тех, к которым есть обращения (вызовы). Аналогичная ситуация и с методами объектов, то есть в результативный код войдет код только вызываемых методов объекта, что позволит создавать эффективный код программ.
В дальнейшем для тестирования объектов будем использовать следующую программу:
Uses profiler, Graph;
var GraphDriver, GraphMode, ErrorCode : Integer;
type
TLine = object
…
procedure InitGrp;
begin
{ драйвер и режимы по умолчанию }
GraphDriver := Detect;
{ драйвер egavga.bgi в текущей папке }
InitGraph(GraphDriver, GraphMode, 'egavga.bgi');
ErrorCode := GraphResult;
{ обработка ошибки }
if ErrorCode <> grOK then begin
Writeln('Graphics error: ', GraphErrorMsg(ErrorCode));
CloseGraph;
Halt(1);
end;
end;
var
Line : TLine;
j : integer;
Begin
InitGrp;
Line.Create(100,100,200,200);
Line.Enable;
readln;
for j:=1 to 100 do begin
Line.Move(100+j,100);
{временная задержка 25 мс}
NewDelay(25);
end;
readln;
CloseGraph;
End.
где для вывода на графический экран будем использовать возможности модуля Graph из пакета Turbo Pascal.
При запуске программы на экране появится белая линия, а после нажатия на клавишу ENTER линия будет плавно двигаться вправо. Выполнение программы завершается еще одним нажатием той же клавиши.
Наследование и переопределениеИтак, ранее мы сконструировали объект TLine, управляющий линией на экране дисплея и это получилось весьма недурственно. Но что делать, если нам понадобится создать объект для управления прямоугольником, такой себе рамкой на экране. Что же нам опять составлять новый объектный тип и опять переписывать кучу кода? Нет уж, давайте воспользуемся наследованием. Что это такое? О, это такая фишка, всем фишкам фишка ;o) Ну а если серьезно, то в Turbo Pascal имеется возможность объявлять новые объектные типы как потомки уже существующего объектного типа, что позволяет потомкам унаследовать все поля и методы родителя (предка), а так же дополнять объекты новыми полями и заменять (переопределять, перекрывать) методы родителя или дополнять их. Вот это собственно и есть наследование.
Чтобы описать объект-потомок достаточно после служебного слова object указать в круглых скобках идентификатор типа объекта-родителя:
type
TFrame = object (TLine)
FFilled : boolean;
function Filled : boolean;
end;
что будет означать, что все поля и методы родителя TLine присутствуют неявно, и стало быть присовокупляются к потомку –
родная кровь все таки ;o) При этом, в новый объект можно ввести дополнительные поля и методы, если в этом есть необходимость. Так в потомок TFrame было введено поле FFilled, управляющее заливкой рамки, что позволит получать на экране либо не закрашенный прямоугольник-рамку, либо закрашенный прямоугольник аля “Черный квадрат” Малевича Казимира Севериновича ;o)
В свою очередь, от типа TFrame можно образовать, так сказать, породить потомок, например, TWindow, ведь свойства рамки и окна очень схожи. Тогда объект TFrame будет предком по отношению к TWindow, а TWindow будет прямым потомком объекта TFrame, и косвенным потомком объекта TLine, и стало быть унаследует все поля и методы, которые были описаны в его предках. Такое объектное генеалогическое древо своими ветвями может подниматься высоко и тянуться к небу пока это будет нужно селекционеру, то бишь программисту. И значит, не имеет смысла снова и снова изобретать велосипед, а остается лишь совершенствовать его и нагромождать необходимые надстройки, дополнения, а это экономит время при разработке программ.
При этом действует правило, что один объектный тип может служить предком для неограниченного числа объектовых типов-потомков, но каждый объектный тип может быть потомком только одного родителя, который указывается в скобках.
Вернемся к нашим баранам. Мы остановились на объявлении нового объектного типа TFrame, который помимо всего прочего унаследовал от своего предка TLine методы Enable и Disable. Да, но ведь эти методы рисуют линию, а объект TFrame, по идее, должен рисовать прямоугольник. В этом случае можно поступить по старинке, то есть добавить два метода, которые бы рисовали рамку, и назывались другими именами. Но это было бы отступлением от объектно-ориентированного стиля программирования. Более элегантным решением может послужить переопределение методов Enable и Disable, то есть замена их новыми методами с аналогичными именами, а за одно и переопределим метод Create, чтобы выполнялась инициализация поля FFilled:
type
TFrame = object (TLine)
FFilled : boolean;
procedure Create( ALeft, ATop, ARight, ABottom : integer );
procedure SetFilled( AFilled : boolean );
procedure Enable;
procedure Disable;
function Filled : boolean;
end;
procedure TFrame.Create( ALeft, ATop, ARight, ABottom : integer );
begin
{ вызываем родительский метод }
TLine.Create( ALeft, ATop, ARight, ABottom );
FFilled := false;
end;
procedure TFrame.SetFilled( AFilled : boolean );
…
Пока приведу лишь исходный код метода Create, а остальные методы будут рассмотрены позднее.
Выше мы переопределили методы, тем самым перекрыли родительские новыми методами. При этом в объекте-потомке под теми же именами будут известны только его собственные методы, а унаследованные (inherited) методы доступны посредством указания составного имени, состоящего из имени родительского типа и имени метода, разделенного точкой.
Из нового метода Create видно, что в нем используется вызов унаследованного метода TLine.Create, чтобы не делать двойную работу. Начиная с версии Turbo Pascal 7.0, введено служебное слово inherited, которое позволяет осуществить вызов перекрытого унаследованного метода. Тогда для седьмой версии метод Create может иметь следующий вид:
procedure TFrame.Create( ALeft, ATop, ARight, ABottom : integer );
begin
{ вызываем родительский метод }
inherited Create( ALeft, ATop, ARight, ABottom );
FFilled := false;
end;
что выглядит гораздо нагляднее.
Помимо этого, новый метод может иметь совсем другой интерфейс входных параметров, чем у одноименного унаследованного метода.
В отличие от методов, поля переопределить нельзя, так как они в обязательном порядке наследуются объектом-потомком, хотя можно добавлять новые поля, но при этом их имена не должны совпадать с именами полей родителя.
Совместимость объектных типовКроме эквивалентных объектных типов, объявленных как type TObj1=TObj2, совместимыми по присваиванию являются объектные типы, состоящие в родстве, при этом можно присваивать потомка родителю:
var
Line : TLine;
Frame : TFrame;
begin
Line := Frame;
…
Такое же правило распространяется и на указатели объектных типов. При подобном присвоении значениями полей объекта-потомка модифицируются только одноименные поля объекта-родителя.
Продолжение следует…
© Владислав Демьянишин
Журнал > Программирование > Паскаль для новичков (Turbo Pascal, Assembler) > Паскаль для новичков (часть 33): Средства объектно-ориентированного программирования
| ||||||||||||||||
|
||||||||||||||||