Объектно-ориентированное программирование в Delphi (ООП)

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

Поясним это на примере. Далее показан стандартный способ работы с данными в структурных приложениях.

Листинг 10.1. Работа с записями 

01.program Project1;
02.{$APPTYPE CONSOLE}
03.uses
04.SysUtils;
05.type
06.TAnimal = record
07.Name: string;
08.Age: Integer;
09.Hungry: Boolean;
10.end;
11.procedure ShowInfo(var AAnimal: TAnimal);
12.const
13.HUNGRY_STRING: array[Boolean] of string = ('No''Yes');
14.begin
15.with AAnimal do
16.begin
17.WriteLn('Name: ', Name);
18.WriteLn('Age: ', Age);
19.WriteLn('Hungry: ', HUNGRY_STRING[Hungry]);
20.end// with AAnimal
21.end;
22.var
23.MyDog: TAnimal;
24.begin
25.MyDog.Name := 'Apollo';
26.MyDog.Age := 10;
27.MyDog.Hungry := False;
28.ShowInfo(MyDog);
29.ReadLn;
30.end.

 

 

 

Чтобы получить доступ к данным, хранящимся в записи MyDog, вы должны передать эту запись процедуре в качестве параметра-переменной. Каждая процедура, которой нужны данные, хранящиеся в записи типа TAnimal, должна иметь параметр, который будет принимать указатель (параметры-переменные действуют как указатели) на фактические данные.

Такой подход порождает три нежелательных побочных эффекта: во-первых, процедуре или функции нужно всегда передавать указатель на запись; во-вторых, необходимо подготовить код, который будет проверять достоверность данных; в-третьих, нужно проверять, существуют ли эти данные на самом деле.

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

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

Листинг 10.2. Доступ к записям с помощью указателей

 

01.program Project1;
02.{$APPTYPE CONSOLE}
03.uses
04.SysUtils;
05.type
06.PAnimal = ^TAnimal; { pointer to a TAnimal record }
07.TAnimal = record
08.Name: string;
09.Age: Integer;
10.Hungry: Boolean;
11.end;
12.procedure ShowInfo(AAnimal: PAnimal);
13.const
14.HUNGRY_STRING: array[Boolean] of string = ('No''Yes');
15.begin
16.if AAnimal = nil then Exit; 
17.with AAnimal^ do
18.begin
19.WriteLn('Name: ', Name);
20.WriteLn('Age: ', Age);
21.WriteLn('Hungry: ', HUNGRY_STRING[Hungry]);
22.end// with AAnimal
23.end;
24.var
25.MyDog: TAnimal;
26.MyDynamicCat: PAnimal;
27.begin
28.MyDog.Name := 'Apollo';
29.MyDog.Age := 10;
30.MyDog.Hungry := False;
31.ShowInfo(@MyDog);
32.New(MyDynamicCat);
33.ShowInfo(MyDynamicCat);
34.Dispose(MyDynamicCat);
35.ReadLn;
36.end.

 


Ключ к решению обозначенных трех проблем лежит в ООП. и особенно — в инкапсуляции. Чтобы не зависеть от указателей на действительные данные и не проверять достоверность этих данных, можно инкапсулировать (проще говоря, "вложить") запись TAnimal и процедуру Showlnfo в класс TAnimal.

Прежде чем продолжить разговор далее, следует обратить внимание на употребление термина "класс". В ООП классы представляют собой модели объектов, подобно тому, как в геноме человека заложена модель человеческого организма. Объект представляет собой экземпляр класса, или реальный продукт моделирования. Если бы этих моделей не было, природные механизмы не позволили бы нам продолжать свой род. а компилятор Delphi не смог бы создавать экземпляры объектов.

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

 

1.type
2.TAnimal = class
3.Name: string;
4.Age: Integer;
5.Hungry: Boolean; end;

 

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

1.var
2.MyDog: TAnimal;

 

 

Однако, это всего лишь объявление объекта. Чтобы объект MyDog можно было на самом деле использовать в приложении, мы должны его создать.

Объекты создаются с помощью конструктора — специального метода, называемого Create, который используется для создания экземпляров объектов. Синтаксическая структура создания объекта выглядит следующим образом:

 

1.Объект := Класс.Create;

 

Чтобы создать объект MyDog, мы должны вызвать конструктор TAnimal, поскольку переменная MyDog объявлена как TAnimal:

 

1.MyDog := TAnimal.Create;

 

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

 

1.MyDog.Name := 'Apollo'; WriteLn(MyDog.Name) ;

 

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

Чтобы уничтожить объект, вы должны вызвать деструктор объекта. Деструктором называется специальный метод Destroy, который используется для освобождения памяти от объекта. Вызывать метод Destroy напрямую не следует ни при каких обстоятельствах. Взамен необходимо вызывать метод Free, поскольку он, прежде чем уничтожать объект, проверяет, существует ли он. Несуществующий объект ссылается на недействительную ячейку памяти, поэтому обращение к такой ячейке приведет к возникновению ошибки.

 

При вызове методов Destroy или Free необходимо ссылаться на объект, а не на его класс:

 

1.MyDog.Destroy; MyDog.Free;

 

На примере кода в листинге 10.3 показано правильное использование объекта.

Листинг 10.3. Использование объекта

 

01.program Project1;
02.{$APPTYPE CONSOLE}
03.uses
04.SysUtils;
05.type
06.TAnimal = class
07.Name: string;
08.Age: Integer;
09.Hungry: Boolean;
10.end;
11.var
12.MyDog: TAnimal;
13.begin
14.{ create the object }
15.MyDog := TAnimal.Create;
16.{ use it }
17.MyDog.Name := 'Apollo';
18.WriteLn(MyDog.Name);
19.{ destroy it }
20.MyDog.Free;
21.ReadLn;
22.end.

 


Для полного завершения процесса инкапсуляции мы должны инкапсулировать процедуру Showlnfo. Инкапсуляция процедуры Showlnfo означает, что она будет объявлена как часть класса TAnimal:

 

1.type
2.TAnimal = class Name: string; Age: Integer; Hungry: Boolean; procedure Showlnfo;
3.end;

 

Имейте в виду, что это всего лишь объявление процедуры Showlnfo. Нам еще нужно написать реализацию метода. В Delphiреализация метода должна находиться за пределами блока класса (в отличие от С++, где допускается как встраиваемая, так и внешняя реализация метода).

 

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

 

1.procedure Класс.Метод(Список_Параметров); function Класс.Метод(Список_Параметров);

 

Следовательно, реализация метода Showlnfo будет выглядеть следующим образом:

 

 

1.procedure TAnimal.Showlnfo;
2.begin
3.end;

 

Метод Showlnfo не имеет списка параметров, поскольку все необходимые ему данные инкапсулированы в классе. Далее показан пример окончательной реализации метода Showlnfo:

 

1.procedure TAnimal.Showlnfo* const
2.HUNGRY_STRING: array[Boolean]  of string =  ('No1,   'Yes'); begin
3.WriteLn('Name :   ', Name); WriteLn(1 Age:   1, Age);
4.WriteLn(1 Hungry :   1,  HUNGRY_STRING[Hungry]); end;

 


Поскольку поля и сам метод Showlnfo принадлежат одному и тому же классу, этот метод может обращаться к полям напрямую. В действительности, метод может обращаться к полям посредством невидимого указателя, называемого Self. КомпиляторDelphi неявным образом создает указатель Self для каждого объекта. Этот указатель ссылается на объект, в котором производится вызов метода; это значит, что с помощью указателя Self объект может ссылаться на самого себя (что следует из самого названия указателя: существительное self в английском языке употребляют, когда имеют в виду себя лично — прим. перев.). При желании, вы можете использовать указатель Self в методе Showlnfo для явной ссылки на поля класса TAnimal:

 

1.procedure TAnimal.Showlnfo; const
2.HUNGRY_STRING: array[Boolean]  of string -  ('No',   'Yes'); begin
3.WriteLn('Name:   ', Self.Name); WriteLn('Age:   \  Self.Age);
4.WriteLn(1 Hungry :   1, HUNGRY_STRING[Self.Hungry]); end;

 


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

 

Листинг 10.4. Полный код класса

 

01.program Project1;
02.{$APPTYPE CONSOLE}
03.uses
04.SysUtils;
05.type
06.TAnimal = class
07.Name: string;
08.Age: Integer;
09.Hungry: Boolean;
10.procedure ShowInfo;
11.end;
12.procedure TAnimal.ShowInfo;
13.const
14.HUNGRY_STRING: array[Boolean] of string = ('No''Yes');
15.begin
16.WriteLn('Name: ', Name);
17.WriteLn('Age: ', Age);
18.WriteLn('Hungry: ', HUNGRY_STRING[Hungry]);
19.end;
20.var
21.MyDog: TAnimal;
22.begin
23.{ create the object }
24.MyDog := TAnimal.Create;
25.{ use it }
26.MyDog.Name := 'Apollo';
27.MyDog.Age := 10;
28.MyDog.Hungry := True;
29.MyDog.ShowInfo;
30.{ destroy it }
31.MyDog.Free;
32.ReadLn;
33.end.

 


С инкапсуляцией записи TAnimal и процедуры Showlnfo мы покончили, однако умудрились сами себе оказать "медвежью услугу''. Мы создали класс в главном файле проекта, а классы должны всегда оставаться в отдельных модулях.

Чтобы переместить класс в новый модуль, потребуется предпринять следующие действия:

  • Создать новый модуль (с именем Animal).
  • Переместить объявление класса в раздел interface (раздел интерфейса) модуля.
  • Переместить реализацию (или реализации) метода в раздел implementation (раздел реализации) модуля.

 

В конечном счете модуль Animal. pas должен выглядеть примерно так, как показано в листинге 10.5.

Листинг 10.5. Модуль класса TAnimal

 

01.unit Animal;
02.interface
03.type
04.TAnimal = class
05.Name: string;
06.Age: Integer;
07.Hungry: Boolean;
08.procedure ShowInfo;
09.end;
10.implementation
11.procedure TAnimal.ShowInfo;
12.const
13.HUNGRY_STRING: array[Boolean] of string = ('No''Yes');
14.begin
15.WriteLn('Name: ', Name);
16.WriteLn('Age: ', Age);
17.WriteLn('Hungry: ', HUNGRY_STRING[Hungry]);
18.end;
19.end.

 

 

Классы, подобно животным и растениям из реального мира, обладают способностью наследования свойств и методов от своих предшественников, или родителей. Однако классы не просто могут наследовать функциональные особенности родительского класса — они делают это всегда. Если вы случайно забудете указать родительский класс в объявлении класса, то Delphiнеявным образом породит его от первоначального родителя — класса TObject. Чтобы явным образом описать родительский класс, вы должны указать имя класса после зарезервированного слова class:

 

1.ТНовый_Класс - class (гТРодительский_Класс)

 

Например, используемый ранее класс TAnimal неявным образом порожден от класса TObject. При желании вы можете также явным образом описать родительский класс класса TAnimal:

 

1.type
2.TAnimal = class(TObject) Name: string; Age:  Integer; Hungry: Boolean;
3.end;

 

Единственная причина, по которой мы можем реализовать экземпляры объектов типа TAnimal. состоит в том. что класс TAnimal наследует необходимые методы от класса TObject. Класс TObject инкапсулирует важные методы, необходимые для создания и уничтожения объектов, такие как Create, Destroy и Free.

Чтобы посмотреть весь список методов, унаследованных от класса TObject (или любого другого родительского класса), можете воспользоваться инструментом Code Insight (Анализатор кода) окна Code Editor (Редактор кода) (рис. 10.1).

Рис. 10.1. Методы, унаследованные от класса TObject 

 

Чтобы отобразить список свойств и методов, принадлежащих объекту, введите имя объекта, поставьте точку и нажмите комбинацию клавиш <Сгг1+Пробел>. Класс TAnimal тоже может использоваться как родительский класс для других классов. Например, он может выступать в качестве родителя для классов TBird и TFish.

 

01.type
02.TAnimal = class
03.Name: string;
04.Age: Integer;
05.Hungry: Boolean;
06.procedure Showlnfo; end;
07.TFish = class(TAnimal)
08.Carnivorous: Boolean;
09.Length:  Integer; end;
10.TBird = class(TAnimal)
11.CanFly: Boolean;
12.end;

 

Оба класса. TFish и TBird. не только наследуют функциональные особенности класса TAnimal, но и расширяют возможности исходного класса за счет новых свойств, таких как Length и CanFly:

1.var
2.F: TFish; begin
3.F := TFish.Create;
4.F.Name := 'Rainbow Trout';
5.F.Length := 54;
6.F.ShowInfo;
7.F.Free; end

 

Рассказывать о полиморфизме, не приводя характерные примеры кода, будет довольно сложно, поэтому подробно о нем речь пойдет чуть позже. А пока что полиморфизм можно охарактеризовать как возможность доступа к объектам различных типов через общего предка.
Например, следующую процедуру можно использовать для изменения свойства Name в объектах типа TAnimal:
procedure SetAnimalName(AAnimal: TAnimal; const AName: string); begin
AAnimal.Name := AName; end;
Вместе с объектами типа TAnimal полиморфизм позволяет передавать объекты типа TBird и TFish процедуре SetAnimalName. поскольку оба класса происходят от общего предка — класса TAnimal.
Листинг 10.6. Полиморфные вызовы
01.program Projectl;
02.($APPTYPE CONSOLE}
03.uses
04.SysUtils,
05.Animal in   'Animal.pas 1;
06.var
07.F: TFish;
08.A: TAnimal;
09.B: TBird;
10.begin
11.F := TFish.Create; A := TAnimal.Create; В := TBird.Create;
12.SetAnimalName(F,   1 Fish1); SetAnimalName(A,   'Animal1); SetAnimalName(B,   'Bird');
13.A.Free; B.Free; F.Free; end.
И последнее, на что необходимо обратить внимание в отношении процедуры SetAnimalName, это параметр AAnimal. Он является стандартным параметром, передаваемым по значению. Чтобы изменить свойства объектов, параметры объектов не нужно объявлять как переменные. Если вы объявите параметр объекта как переменную, вы тем самым организуете передачу указателя на указатель. Это приведет только к замедлению работы приложения и к нежелательному усложнению исходного кода.
dle

Помоги проекту! Расскажи друзьям об этом сайте: