1. Инкапсуляция
Объединение в объекте кода и данных
называется инкапсуляцией. В принципе, возможно предоставить достаточное
количество методов, благодаря которым пользователь объекта никогда не будет
обращаться к полям объекта непосредственно. Некоторые другие
объектно-ориентированные языки, например Smalltalk, требуют обязательной
инкапсуляции, однако в Borland Pascal имеется выбор.
Например, объекты TEmployee и THourly
написаны таким образом, что совершенно исключена необходимость прямого
обращения к их внутренним полям данных:
type
TEmployee
= object
Name,
Title: string[25];
Rate:
Real;
procedure
Init (AName, ATitle: string; ARate: Real);
function
GetName : String;
function
GetTitle : String;
function
GetRate : Real;
function
GetPayAmount : Real;
end;
THourly
= object(TEmployee)
Time:
Integer;
procedure
Init(AName, ATitle: string; ARate:
Real,
Atime: Integer);
function
GetPayAmount : Real;
end;
Здесь присутствуют только четыре поля
данных: Name, Title, Rate и Time. Методы GetName и GetTitle выводят фамилию
работающего и его должность соответственно. Метод GetPayAmount использует Rate,
а в случае работающего THourly и Time для вычисления суммы выплат работающему.
Здесь уже нет необходимости обращаться непосредственно к этим полям данных.
Предположив существование экземпляра
AnHourly типа THourly, мы могли бы использовать набор методов для
манипулирования полями данных AnHourly например:
with
AnHourly do
begin
Init
(Aleksandr Petrov, Fork lift operator' 12.95, 62);
{Выводит на экран фамилию, должность и
сумму выплат}
Show;
end;
Следует обратить внимание, что доступ к
полям объекта осуществляется не иначе, как только с помощью методов этого
объекта.
2. Расширяющиеся
объекты
К сожалению, стандартный Pascal не
предоставляет никаких возможностей для создания гибких процедур, позволяющих
работать с абсолютно разными типами данных. Объектно-ориентированное
программирование решает эту проблему с помощью наследования: если определен
порожденный тип, то методы порождающего типа наследуются, однако при желании
они могут переопределяться. Для переопределения наследуемого метода попросту
описывается новый метод с тем же именем, что и наследуемый метод, но с другим
телом и (при необходимости) с другим множеством параметров.
Определим дочерний по отношению к TEmployee
тип, представляющий работника, которому платится часовая ставка, в следующем
примере:
const
PayPeriods = 26; { периоды выплат }
OvertimeThreshold = 80; { на период
выплаты }
OvertimeFactor
= 1.5; { почасовой коэффициент }
type
THourly
= object(TEmployee)
Time:
Integer;
procedure
Init(AName, ATitle: string; ARate:
Real,
Atime: Integer);
function
GetPayAmount : Real;
end;
procedure
THourly.Init(AName, ATitle: string;
ARate:
Real, Atime: Integer);
begin
TEmployee.Init(AName,
ATitle, ARate);
Time
:= ATime;
end;
function
THourly.GetPayAmount: Real;
var
Overtime:
Integer;
begin
Overtime
:= Time – OvertimeThreshold;
if
Overtime > 0 then
GetPayAmount
:= RoundPay(OvertimeThreshold * Rate +
Rate
OverTime * OvertimeFactor * Rate)
else
GetPayAmount
:= RoundPay(Time * Rate)
end;
Человек, которому платится часовая ставка,
является работающим: он обладает всем тем, что используется для определения
объекта TEmployee (фамилией, должностью, ставкой), и лишь количество получаемых
почасовиком денег зависит от того, сколько часов он отработал за период,
подлежащий оплате. Таким образом, для THourly требуется еще и поле времени
Time.
Так как THourly определяет новое поле Time,
его инициализация требует нового метода Init, который инициализирует и время, и
наследованные поля. Вместо того, чтобы непосредственно присвоить значения
наследованным полям, таким как Name, Title и Rate, почему бы не использовать
вновь метод инициализации объекта TEmployee (иллюстрируемый первым оператором
THourly Init).
Вызов метода, который переопределяется, не
является лучшим стилем. В общем случае возможно, что TEmployee.Init выполняет
важную, однако скрытую инициализацию.
Вызывая переопределяемый метод, необходимо
быть уверенным в том, что порожденный тип объекта включает функциональность
родителя. Кроме того, любое изменение в родительском методе автоматически
оказывает влияние на все порожденные.
После вызова TEmployee.Init, THourly.Init
может затем выполнить свою собственную инициализацию, которая в этом случае
состоит только в присвоении значения, переданного в ATime.
Другим примером переопределяемого метода
является функция THourly.GetPayAmount, вычисляющая сумму выплат работающему на
почасовой ставке. В действительности, каждый тип объекта TEmployee имеет свой
метод GetPayAmount, так как тип работающего зависит от того, как производится
расчет. Метод THourly.GetPayAmount должен учитывать, сколько часов работал
сотрудник, были ли сверхурочные работы, каков коэффициент увеличения за
сверхурочные работы и т. д.
Метод TSalaried. GetPayAmount должен лишь
делить ставку работающего на число выплат в каждом году (в нашем примере).
unit
Workers;
interface
const
PayPeriods
= 26; {в год}
OvertimeThreshold = 80; {за каждый период
оплаты}
OvertimeFactor =1.5; {увеличение против
обычной оплаты}
type
TEmployee
= object
Name,
Title: string[25];
Rate:
Real;
procedure
Init (AName, ATitle: string; ARate: Real);
function
GetName : String;
function
GetTitle : String;
function
GetRate : Real;
function
GetPayAmount : Real;
end;
THourly
= object(TEmployee)
Time:
Integer;
procedure
Init(AName, ATitle: string; ARate:
Real,
Atime: Integer);
function
GetPayAmount : Real;
function
GetTime : Real;
end;
TSalaried
= object(TEmployee)
function
GetPayAmount : Real;
end;
TCommissioned
= object(TSalaried)
Commission
: Real;
SalesAmount
: Real;
constructor
Init (AName, ATitle: String; ARate,
ACommission,
ASalesAmount: Real);
function
GetPayAmount : Real;
end;
implementation
function
RoundPay(Wages: Real) : Real;
{округляем сумму выплат, чтобы
игнорировать суммы меньше
денежной единицы}
begin
RoundPay
:= Trunc(Wages * 100) / 100;
.
.
.
TEmployee является вершиной нашей иерархии
объектов и содержит первый метод GetPayAmount.
function
TEmployee.GetPayAmount : Real;
begin
RunError(211);
{ дать ошибку этапа выполнения }
end;
Может вызвать удивление тот факт, что метод
дает ошибку времени выполнения. Если вызывается Employee.GetPayAmount, то в
программе возникает ошибка. Почему? Потому что TEmployee является вершиной
нашей иерархии объектов и не определяет реального рабочего; следовательно, ни
один из методов TEmployee не вызывается определенным образом, хотя они и могут
быть наследованными. Все наши работники являются либо почасовиками, либо имеют
оклады, либо работают на сдельщине. Ошибка на этапе выполнения прекращает
выполнение программы и выводит 211, что соответствует сообщению об ошибке,
связанной с вызовом абстрактного метода (если программа по ошибке вызывает
TEmployee.GetPayAmount).
Ниже приводится метод THourly.GetPayAmount,
в котором учитываются такие вещи, как сверхурочная оплата, число отработанных
часов и т. д.
function
THourly.GetPayAMount : Real;
var
OverTime:
Integer;
begin
Overtime
:= Time – OvertimeThreshold;
if
Overtime > 0 then
GetPayAmount
:= RoundPay(OvertimeThreshold * Rate +
Rate
OverTime * OvertimeFactor * Rate)
else
GetPayAmount
:= RoundPay(Time * Rate)
end;
Метод TSalaried.GetPayAmount намного
проще; в нем ставка
делится на число выплат:
function
TSalaried.GetPayAmount : Real;
begin
GetPayAmount
:= RoundPay(Rate / PayPeriods);
end;
Если взглянуть на метод
TCommissioned.GetPayAmount, то будет видно, что он вызывает
TSalaried.GetPayAmount, вычисляет комиссионные и прибавляет их к величине,
возвращаемой методом TSalaried. GetPayAmount.
function
TСommissioned.GetPayAmount : Real;
begin
GetPayAmount
:= RoundPay(TSalaried.GetPayAmount +
Commission * SalesAmount);
end;
Важное замечание: хотя методы могут быть
переопределены, поля данных переопределяться не могут. После того как было
определено поле данных в иерархии объекта, никакой дочерний тип не может
определить поле данных в точности с таким же именем.
3. Совместимость
типов объектов
Наследование до некоторой степени изменяет
правила совместимости типов в Borland Pascal. Помимо всего прочего, порожденный
тип наследует совместимость типов всех своих порождающих типов.
Эта расширенная совместимость типов
принимает три формы:
1) между реализациями объектов;
2) между указателями на реализации
объектов;
3) между формальными и фактическими
параметрами.
Однако очень важно помнить, что во всех трех
формах совместимость типов расширяется только от потомка к родителю. Другими
словами, дочерние типы могут свободно использоваться вместо родительских, но не
наоборот.
Например, TSalaried является потомком
TEmployee, а ТСош-missioned – потомком TSalaried. Помня об этом, рассмотрим
следующие описания:
tyрe
PEmрloyee = ^TEmployee;
PSalaried = ^TSalaried;
PCommissioned
= ^TCommissioned;
var
AnEmрloyee: TEmployee;
ASalaried:
TSalaried;
PCommissioned:
TCommissioned;
TEmployeePtr:
PEmрloyee;
TSalariedPtr:
PSalaried;
TCommissionedPtr:
PCommissioned;
При данных описаниях справедливы следующие операторы
присваивания:
AnEmрloyee :=ASalaried;
ASalaried
:= ACommissioned;
TCommissionedPtr := ACommissioned;
Примечание
Порождающему объекту можно присвоить
экземпляр любого из его порожденных типов. Обратные присваивания недопустимы.
Эта концепция является новой для Pascal, и
вначале, возможно, трудновато запомнить, в каком порядке следует совместимость
типов. Необходимо думать следующим образом: источник должен быть в состоянии
полностью заполнить приемник. Порожденные типы содержат все, что содержат их
порождающие типы благодаря свойству наследования. Поэтому порожденный тип имеет
либо в точности такой же размер, либо (что чаще всего и бывает) он больше
своего родителя, но никогда не бывает меньше. Присвоение порождающего
(родительского) объекта порожденному (дочернему) могло бы оставить некоторые
поля порожденного объекта неопределенными, что опасно и поэтому недопустимо.
В операторах присваивания из источника в
приемник будут копироваться только поля, являющиеся общими для обоих типов. В
операторе присваивания:
AnEmployee:= ACommissioned;
Только поля Name, Title и Rate из
ACommissioned будут скопированы в AnEmployee, так как только эти поля являются
общими для TCommissioned и TEmployee. Совместимость типов работает также между
указателями на типы объектов и подчиняется тем же общим правилам, что и для
реализаций объектов. Указатель на потомка может быть присвоен указателю на
родителя. Если дать предыдущие определения, то следующие присваивания указателей
будут допустимыми:
TSalariedPtr:=
TCommissionedPtr;
TEmployeePtr:=
TSalariedPtr;
TEmployeePtr:=
PCommissionedPtr;
Следует помнить, что обратные присваивания
недопустимы!
Формальный параметр (либо значение, либо
параметр-переменная) данного объектного типа может принимать в качестве
фактического параметра объект своего же типа или объекты всех дочерних типов.
Если определить заголовок процедуры следующим образом:
procedure CalcFedTax(Victim: TSalaried);
то допустимыми типами фактических параметров
могут быть TSalaried или TCommissioned, но не тип TEmployee. Victim также может
быть параметром-переменной. При этом выполняются те же правила совместимости.
Замечание
Между параметрами-значениями и
параметрами-переменными есть коренное отличие. Параметр-значение является
указателем на действительный, посьшаемый в качестве параметра объект, тогда как
параметр-переменная является только копией фактического параметра. Более того,
эта копия включает только те поля, которые входят в тип формального параметра-значения.
Это означает, что фактический параметр буквально преобразуется к типу
формального параметра. Параметр-переменная больше напоминает приведение к
образцу, в том смысле, что фактический параметр остается неизменным.
Аналогично, если формальный параметр
является указателем на тип объекта, фактический параметр может быть указателем
на этот тип объекта или на любой дочерний тип. Пусть дан заголовок процедуры:
procedure Worker.Add (AWorker:
PSalaried);
Тогда допустимыми типами фактических
параметров могут быть PSalaried или PCommissioned, но не тип PEmployee.
|