Песни о Паскале — страница 98 из 112

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

Объявление объекта

Объявление объекта похоже на объявление записи, с той разницей, что ключевое слово RECORD заменяют словом OBJECT. В Delphi и совместимом с ним режиме Free Pascal применяют ключевое слово CLASS. Итак, «застолбим» место хранения информации о человеке тремя полями, как это показано ниже.


type TPerson = object

      mBearing : integer; { год рождения }

      mName : string;       { имя }

      mFam : string;       { фамилия }

      end;


Здесь объявлен тип данных TPerson (персона), содержащий три поля с данными о человеке. Для распечатки данных учредим процедуру по имени Report. Но процедура эта особая! Её заголовок помещен внутрь описания объекта следующим образом:


type TPerson = object

      mBearing : integer; { год рождения }

      mName : string;       { имя }

      mFam : string;       { фамилия }

procedure Report;       { процедура распечатки объекта }

      end;


Методы

Процедуры и функции, объявленные внутри объекта, называют методами объекта. Методы, как и поля, – неотъемлемая часть объекта. Но объявить метод недостаточно, надо создать и его тело или, как принято говорить, реализацию метода. Реализация – это подпрограмма (процедура или функция), которая от обычной подпрограммы отличается заголовком: там к имени метода добавляется приставка, указывающая тип объекта, которому принадлежит метод. Реализация метода Report будет очень простой.


procedure TPerson.Report;

begin

      Writeln(mBearing:6, 'Фамилия: '+mFam:20, ' Имя: '+mName);

end;


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

Инициализация, конструктор

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


var P : TPerson;       { переменная-объект }

begin

      P.mFam:=’Сидоров’;

      P.mName:= ’Тимофей’;

end.


Но, когда полей много, вы забудете что-то – в этом слабость идеи. Куда надежней учредить особый метод для инициализации полей. Такой метод и назван особо – конструктор. Вместо слова PROCEDURE перед именем конструктора так и пишут: CONSTRUCTOR. Назвать конструктор можно как угодно, но по традиции ему дают имена Init (инициализировать) или Create (создать). Например, для нашего объекта объявить конструктор и реализовать его тело можно так:


type TPerson = object

      {... }

      { заголовок конструктора внутри объекта }

constructor Init(aBearing: integer; const aName, aFam : string);

      end;

      { реализация конструктора }

constructor TPerson.Init(aBearing: integer; const aName, aFam : string);

begin

      mBearing:= aBearing; mName:= aName; mFam:= aFam;

end;


В этом примере конструктор Init копирует три своих параметра в поля объекта. Теперь переменную-объект P можно инициализировать вызовом конструктора.


var P : TPerson;       { переменная-объект }

begin

      P.Init(1995, 'Мария', 'Рыбкина');


Так ни одно поле объекта не будет пропущено, – за этим присмотрит компилятор!

Вот пока все, что следует сказать об инкапсуляции. Приведенный ниже пример «P_61_2» демонстрирует объект типа TPerson: здесь описана его структура и реализация методов, а затем объявлены две переменные, выполнена их инициализация и распечатка полей.


      { P_61_2 Программа с применением объекта типа «человек» (персона) }

type TPerson = object

      mBearing : integer; { год рождения }

      mName : string;       { имя }

      mFam : string;       { фамилия }

      constructor Init(aBearing: integer; const aName, aFam : string);

      procedure Report;       { процедура распечатки объекта }

      end;

      {--- Реализация двух методов объекта ---}

constructor TPerson.Init(aBearing: integer; const aName, aFam : string);

begin

      mBearing := aBearing; mName := aName;       mFam := aFam;

end;

procedure TPerson.Report;

begin

      Writeln(mBearing:6, 'Фамилия: '+mFam:20, ' Имя: '+mName);

end;

var P1, P2 : TPerson; { две переменных объектного типа }

begin       {--- Главная программа ---}

      P1.Init(1985, 'Иван', 'Грозный');

      P2.Init(1995, 'Мария', 'Рыбкина');

      P1.Report;

      P2.Report;

      Readln;

end.


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

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

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

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

И это не все. Постройка одних объектов на основе других формирует иерархию родственных объектов. С разными объектами в этой иерархии можно обращаться сходным образом, – это и есть полиморфизм. Буквальный перевод этого слова – «многоструктурность» – почти ничего не объясняет. Принципы наследования и полиморфизма легче понять на примере.

Приборостроение

Я знаю, чем напичкан ваш дом, – электрическими приборами. И простыми, такими, как лампочка или утюг. И сложными, – телевизор, стиральная машина, компьютер, наконец. Взглянем на них глазами программиста: любой такой прибор, выражаясь языком ООП, обладает, по крайней мере, двумя общими «методами» – включить и отключить. В разных приборах эти операции выполняются по-разному, но в целом они сходны. Можно сказать, что эти методы – общие свойства всех электроприборов.

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


type Электроприбор = object

      procedure Включить; virtual;

      procedure Отключить; virtual;

      end;


Здесь встречаем новое «волшебное» словечко – VIRTUAL, что значит «воображаемый». Это ключевое слово Паскаля следует за объявлением тех методов объекта, которые разрешено изменять в его наследниках. Изменение метода в наследниках называют переопределением метода. Итак, слово VIRTUAL указывает компилятору, что в наследниках методы включения и отключения прибора могут быть изменены в соответствии с особенностями этих наследников. К примеру, лампочка и телевизор включаются по-разному.

Основав абстрактный электроприбор, построим на нём прибор «чисто конкретный», например, телевизор.


type Телевизор = object (Электроприбор)

      procedure Включить;