Основные принципы ООП

История развития ООП

Исторически сложилось так, что программирование долгие годы развивалось как процедурное программирование, которое предполагает, что основой программы является алгоритм, процедура обработки данных.  Известный швейцарский ученый, специалист в области информатики — Никлаус Вирт выпустил книгу «Алгоритмы + структуры данных = программы», в которой систематически изложил основные понятия о структурах данных и фундаментальных алгоритмах, которые эти данные обрабатывают.

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

ЗАМЕЧАНИЕ

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

Базовые понятия ООП

Класс

Классический язык Pascal позволяет программисту определять свои собственные сложные типы данных — записи (records). Язык Object Pascal, поддерживая концепцию объектно-ориентированного программирования, дает возможность определять классы. Класс — это сложная структура, включающая, помимо описания данных, описание процедур и функций, которые могут быть выполнены над представителем класса — объектом.

Вот пример объявления простого класса:

TPerson = class
private
  FName: string[15];
  FAddress: string[35];
public
  procedure Show;
end;

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

ЗАМЕЧАНИЕ

Согласно принятому в Object Pascal соглашению, имена классов должны начинаться с буквы T (от англ. Type), полей — с буквы F (от англ. field — поле). Описание классов помещают в программе в раздел описания типов (type).

Объект

Объекты как представители класса объявляются в программе в разделе var, например:

var
  student: TPerson;
  professor: TPerson;

ЗАМЕЧАНИЕ

В Object Pascal объект — это динамическая структура. Переменная-объект содержит не данные, а ссылку на данные объекта. Поэтому программист должен позаботиться о выделении памяти для этих данных. Выделение памяти осуществляется при помощи специального метода класса — конструктора, которому обычно присваивают имя Сreate

(Создать). Для того чтобы подчеркнуть особую роль и поведение конструктора, в описании класса вместо слова procedure используется слово constructor.

Далее приведено описание класса TPerson, в состав которого введен конструктор:

TPerson = class
private
  FName: string[15];
  FAddress: string[35];
  constructor Сreate; // конструктор
public
  procedure show; // метод
end;

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

professor := TPerson.Сreate;

выделяется необходимая память для данных объекта professor.

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

constructor TPerson.Сreate;
begin
  FName := '';
  FAddress := '';
end;

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

После объявления и инициализации объект можно использовать, например, установить значение поля объекта. Доступ к полю объекта осуществляется указанием имени объекта и имени поля, которые отделяются друг от друга точкой. Хотя объект является ссылкой, однако, правило доступа к данным с помощью ссылки, согласно которому после имени переменной, являющейся ссылкой, надо ставить значок ^, на объекты не распространяется. Например, для доступа к полю FName объекта professor вместо professor^.Fname надо писать

professor.FName

Очевидно, что такой способ доступа к полям объекта более естественен. Если в программе какой-либо объект больше не используется, то можно освободить память, занимаемую полями данного объекта. Для выполнения этого действия используют метод – деструктор Free. Например, для того чтобы освободить память, занимаемую полями объекта professor, достаточно записать

professor.Free;

Метод

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

professor.Show;

вызывает применение метода Show к объекту professor. Фактически инструкция применения метода к объекту — это специфический способ записи инструкции вызова процедуры.

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

Далее приведен пример определения метода Show класса TPerson:

// метод Show класса Tperson
procedure TPerson.Show;
begin
  ShowMessage('Имя:' + FName + #13 + 'Адрес:' + FAddress);
end;

ЗАМЕЧАНИЕ

В инструкциях метода, доступ к полям объекта осуществляется без указания имени объекта.

Основные принципы ООП

Инкапсуляция и свойства объекта

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

В языке Object Pascal ограничение доступа к полям объекта может реализовываться при помощи свойств объекта. Свойство объекта характеризуется полем, сохраняющим значение свойства, и двумя методами, обеспечивающими доступ к полю свойства. Метод установки значения свойства называется методом записи свойства (write), а метод получения значения свойства — методом чтения свойства (read).

В описании класса перед именем свойства записывают слово property (свойство). После имени свойства указывается его тип, затем — имена методов, обеспечивающих доступ к значению свойства. После слова read указывается имя метода, обеспечивающего чтение свойства, после слова write — имя метода, отвечающего за запись свойства.

Далее приведен пример описания класса TPerson, содержащего два свойства: Name и Address.

type

TName = string[15];
TAddress = string[35];

TPerson = class // класс
private
  FName: TName; // значение свойства Name
  FAddress: TAddress; // значение свойства Address
  Constructor Create(Name:Tname);
  Procedure Show;
  Function GetName: TName;
  Function GetAddress: TAddress;
  Procedure SetAddress(NewAddress:TAddress);
public
  Property Name: Tname // свойство Name
  read GetName; // доступно только для чтения
  Property Address: TAddress // свойство Address
  read GetAddress // доступно для чтения
  write SetAddress; // и записи
end;

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

student.Address := 'С.-Петербург, ул. Садовая, д. 21, кв. 3';

Компилятор перетранслирует приведенную инструкцию присваивания значения свойству в инструкцию вызова метода

student.SetAddress('С.-Петербург, ул. Садовая, д. 21, кв. 3');

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

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

Оформление данных объекта как свойства позволяет ограничить доступ к полям, хранящим значения свойств объекта, например, можно разрешить только чтение. Для того чтобы инструкции программы не могли изменить значение свойства, в описании свойства надо указать лишь имя метода чтения. Попытка присвоить значение свойству, предназначенному только для чтения, вызывает ошибку времени компиляции. В приведенном выше описании класса TPerson свойство Name доступно только для чтения, а свойство Address — для чтения и записи.

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

// конструктор объекта Tperson
Constructor TPerson.Create(Name:TName);
begin
  FName := Name;
end;

// метод получения значения свойства Name
Function TPerson.GetName;
begin
  Result := FName;
end;

// метод получения значения свойства Address
function TPerson.GetAddress;
begin
  Result:=FAddress;
end;

// метод изменения значения свойства Address
Procedure TPerson.SetAddress(NewAddress:TAddress);
begin
  if FAddress = '' then
    FAddress := NewAddress;
end;

Приведенный конструктор объекта TPerson создает объект и устанавливает значение поля FName, определяющего значение свойства Name.

Инструкции программы, обеспечивающие создание объекта класса TPerson и установку его свойства, могут быть, например, такими:

student := TPerson.Сreate('Иванов');
student.Address := 'ул. Садовая, д. 3, кв. 25';

Наследование

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

В объявлении класса-потомка указывается класс родителя. Например, класс TEmployee (сотрудник) может быть порожден от рассмотренного ранее класса TPerson путем добавления поля FDepartment (отдел). Объявление класса TEmployee в этом случае может выглядеть так:

TEmployee = class(TPerson)
  FDepartment: integer; // номер отдела
  constructor Create(Name:TName; Dep:integer);
end;

Заключенное в скобки имя класса TPerson показывает, что класс Temployee является производным от класса TPerson. В свою очередь, класс TPerson является базовым для класса TEmployee.

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

Вот пример реализации конструктора класса TEmployee:

constructor TEmployee.Create(Name:Tname;Dep:integer);
begin
  inherited Create(Name);
  FDepartment:=Dep;
end;

В приведенном примере директивой inherited вызывается конструктор родительского класса. После этого присваивается значение полю класса-потомка.

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

engineer := TEmployee.Сreate('Сидоров',413);
engineer.address := 'ул. Блохина, д. 8, кв. 10';

Первая инструкция создает объект типа TEmployee, вторая — устанавливает значение свойства, которое относится к родительскому классу.

Директивы Protected и Private

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

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

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

Далее приведено описание класса TPerson, в которое включены директивы управления доступом.

TPerson = class
private
  FName: TName; // значение свойства Name
  FAddress: TAddress; // значение свойства Address
protected
  Constructor Create(Name:TName);
  Function GetName: TName;
  Function GetAddress: TAddress;
  Procedure SetAddress(NewAddress:TAddress);
  Property Name: TName read GetName;
  Property Address: TAddress read GetAddress write SetAddress;
end;

ЗАМЕЧАНИЕ

Иногда нужно полностью скрыть элементы класса. В этом случае определение класса следует поместить в отдельный модуль.

Полиморфизм

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

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

type
// базовый класс
TPerson = class
  FName: string; // имя
  constructor Create(name:string);
  function info: string; virtual;
end;

// производный от TPerson
TStud = class(TPerson)
  FGroup:integer; // номер учебной группы
  constructor Create(name:string;gr:integer);
  function info: string; override;
end;

// производный от TPerson
TProf = class(TPerson)
  FDep:string; // название кафедры
  constructor Create(name:string;dep:string);
  function info: string; override;
end;

В каждом из этих классов определен метод info. В базовом классе при помощи директивы virtual метод info объявлен виртуальным. Объявление метода виртуальным дает возможность дочернему классу произвести замену виртуального метода своим собственным. В каждом дочернем классе определен свой метод info, который замещает соответствующий метод родительского класса (метод порожденного класса, замещающий виртуальный метод родительского класса, помечается директивой override).

Далее приведено определение метода info для каждого класса.

function TPerson.info: string;
begin
  result := '';
end;

function TStud.info: string;
begin
  result := FName + ' гр.' + IntTostr(FGroup);
end;

function TProf.info: string;
begin
  result := FName + ' каф.' + FDep;
end;

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

list: array[1..SZL] of TPerson;

Объявить подобным образом список можно потому, что язык Object Pascal позволяет указателю на родительский класс присвоить значение указателя на дочерний класс. Поэтому элементами массива list могут быть как объекты класса TStud, так и объекты класса TProf.

Вывести список студентов и преподавателей можно применением метода info к элементам массива. Например, так:

st := '';
for i:=1 to SZL do // SZL — размер массива-списка
  if list[i] <> NIL then
st := st + list[i].Info + #13;
ShowMessage(st);

Во время работы программы каждый элемент массива может содержать как объект типа TStud, так и объект типа TProf. Концепция полиморфизма обеспечивает применение к объекту именно того метода, который соответствует типу объекта. Следующая программа, используя рассмотренные ранее объявления классов TPerson, TStud и TProf, формирует и выводит список студентов и преподавателей. Текст программы приведен в листинге 1, а диалоговое окно — на рис. 1.

Рис. 1. Диалоговое окно программы Полиморфизм

Листинг 1. Демонстрация полиморфизма

unit polimor_;
interface
uses
  SysUtils, Classes, Graphics, Controls, Forms,  Dialogs, StdCtrls;

type
{ TForm1 }
TForm1 = class(TForm)
  Edit1: TEdit;
  Edit2: TEdit;
  GroupBox1: TGroupBox;
  RadioButton1: TRadioButton;
  RadioButton2: TRadioButton;
  Label1: TLabel;
  Label2: TLabel;
  Button1: TButton;
  Button2: TButton;
  procedure Button1Click(Sender: TObject);
  procedure Button2Click(Sender: TObject);
private { Private declarations }
public { Public declarations }
end;

type
// базовый класс
TPerson = class
  FName: string; // имя
  constructor Create(name:string);
  function info:string; virtual;
end;

// класс Студент
TStud = class(TPerson)
  FGr:integer; // номер группы
  constructor Create(name:string;gr:integer);
  function info:string; override;
end;

// класс Преподаватель
TProf = class(TPerson)
  FDep: string; // название кафедры
  constructor Create(name:string; dep:string);
  function info: string; override;
end;

const
SZL = 10; // размер списка

var
  Form1: TForm1;

List: array[1..SZL] of TPerson; // список
n:integer = 0;                  // кол-во людей в списке

implementation

{$R *.lfm}

constructor TPerson.Create(name:string);
begin
  FName := name;
end;

constructor TStud.Create(name:string;gr:integer);
begin
  inherited create(name); // вызвать конструктор базового класса
  FGr := gr;
end;

constructor TProf.create(name:string; dep:string);
begin
  inherited create(name); // вызвать конструктор базового класса
  FDep := dep;
end;

function TPerson.Info:string;
begin
  result := FName;
end;

function TStud.Info:string;
begin
  result := FName + ' гр.' + IntToStr(FGr);
end;

function TProf.Info:string;
begin
  result := FName + ' каф.' + FDep;
end;

// щелчок на кнопке Добавить
procedure TForm1.Button1Click(Sender: TObject);
begin
  if n < SZL then
  begin           // добавить объект в список
    n:=n+1;
    if Radiobutton1.Checked
    then // создадим объект TStud
      List[n] := TStud.Create(Edit1.Text,StrToInt(Edit2.Text))
    else // создать объект TProf
      List[n]:=TProf.Create(Edit1.Text,Edit2.Text);
    // очистить поля ввода
    Edit1.Text := '';
    Edit2.Text := '';
    Edit1.SetFocus;  // курсор в поле Фамилия
  end
  else ShowMessage('Список заполнен!');
end;

// щелчок на кнопке Список
procedure TForm1.Button2Click(Sender: TObject);
var
  i:integer; // индекс
  st:string=''; // список
begin
  for i:=1 to SZL do
    if list[i] <> NIL then st:=st + list[i].info + #13;
  ShowMessage('Список'+#13+st);
end;

end.

Процедура TForm1.Button1Click, которая запускается нажатием кнопки Добавить (Button1), создает объект list[n] класса TStud или TProf. Класс создаваемого объекта определяется состоянием переключателя RadioButton. Установка переключателя в положение студент (RadioButton1) определяет класс Tstud, а в положение преподаватель (RadioButton2) — класс TProf.

Процедура TForm1.Button2Click, которая запускается нажатием кнопки Список (Button2), применяя метод info к каждому объекту списка(элементу массива), формирует строку, представляющую собой весь список.

Классы объектов.

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

Любой объект имеет одинаковые атрибуты (поля) и функциональность с другими объектами того же класса. За создание своих классов и поведение объектов этих классов полную ответственность несет сам программист. Работая в некоторой среде, программист получает доступ к обширным библиотекам стандартных классов (в Lazarus например — LCL, FCL).

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

Компоненты и их свойства

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

Свойство является важным атрибутом компонента. Для пользователя (программиста) свойство выглядит как простое поле какой-либо структуры, содержащее некоторое значение. Однако, в отличие от «просто» поля, любое изменение значения некоторого свойства компонента сразу же приводит к изменению визуального представления этого компонента, поскольку свойство инкапсулирует в себе методы (действия), связанные с чтением и записью этого поля (которые, в свою очередь, включают в себя необходимую перерисовку). Свойства служат двум главным целям. Во-первых, они определяют внешний вид компонента. А во-вторых, свойства определяют его поведение.

Для реализации пользовательского интерфейса Lazarus использует библиотеку визуальных компонентов (Lazarus Components Library, LCL), которая представляет собой набор классов, например, TForm (форма), TLabel (компонент Label), TEdit (компонент Edit), TButton (компонент Button — командная кнопка) и множество других.

Во время проектирования формы Lazarus автоматически формирует описание класса формы. Если сразу после запуска Lazarus просмотреть в окне редактора кода модуль формы, то там можно обнаружить следующие строки:

type
  TForm1 = class(TForm)
  private
  public
  end;

var
  Form1: TForm1;

Это описание класса исходной, пустой (т. е. не содержащей ни одного компонента) формы приложения и объявление объекта типа TForm1 — формы приложения.

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

TForm1 = class(TForm)
  Edit1: TEdit;
  Edit2: TEdit;
  Button1: TButton;
  Label1: TLabel;
  Label2: TLabel;
  Label3: TLabel;
  Label4: TLabel;
private
public
end;

Когда программист создает функцию обработки, Lazarus добавляет объявление метода в описание класса формы. Ниже показано, как выглядит объявление класса формы после того, как программист создал процедуры обработки события KeyPress для компонентов Edit1 и Edit2, а также события Click на кнопке Button1.

TForm1 = class(TForm)
  Edit1: TEdit;
  Edit2: TEdit;
  Button1: TButton;
  Label1: TLabel;
  Label2: TLabel;
  Label3: TLabel;
  Label4: TLabel;
  procedure Edit1KeyPress(Sender: TObject; var Key: Char);
  procedure Edit2KeyPress(Sender: TObject; var Key: Char);
  procedure Button1Click(Sender: TObject);
private
public
end;

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

Free Component Library (FCL) — бесплатная и свободная библиотека компонентов Free Pascal. Она состоит из набора модулей, предоставляющих классы и компоненты для общих задач. FCL ограничивается только не визуальными компонентами. Lazarus так же имеет собственную библиотеку компонентов —  LCL (Lazarus component library), с которой мы познакомимся.

Однако более подробное рассмотрение этой темы в задачу курса не входит.

Добавить комментарий