Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Инкапсуляция при помощи свойств



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

Классическое правило объектно-ориентированного программирования утверждает, что для обеспечения надежности нежелателен прямой доступ к полям объекта: чтение и обновление их содержимого должно производиться посредством вызова соответствующих методов. Это правило и называется инкапсуляцией. В старых реализациях ООП (например, в Turbo Pascal) эта мысль внедрялась только посредством призывов и примеров в документации: в языке же Object Pascal есть соответствующая конструкция. В Delphi пользователь вашего объекта может быть полностью отгорожен от полей при помощи свойств.

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

TAnObject = class(TObject)

function GetAProperty: TSomeType;

procedure SetAProperty(ANewValue: TSomeType);

property AProperty: TSomeType read GetAProperty write SetAProperty;

end;

В данном примере доступ к значению свойства AProperty осуществляется через вызовы методов GetAProperty и SetAProperty. Однако в обращении к этим методам в явном виде нет необходимости: достаточно написать

AnObject.AProperty: = AValue;

АVariable: = AnObject.AProperty;

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

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

TPropObject = class(TObject) FValue: TSomeType;

procedure DoSomething;

function Correct(AValue: Integer): boolean;

procedure SetValue(NewValue:

Integers-property AValue:

Integer read FValue write SetValue;

end;

procedure TPropObject.SetValue(NewValue: Integer);

begin

if (NewValueOFValue) and Correct (NewValue) then FValue: = NewValue;

DoSomething;

end;

В этом примере чтение значения свойства AValue означает просто чтение поля rvalue. Зато при присвоении значения внутри setvalue вызывается сразу два метода.

Если свойство должно только читаться или только записываться, в его описании может присутствовать только соответствующий метод:

type

TAnObject = class(TObject)

property AProperty: TSomeType read GetValue;

end;

В этом примере вне объекта значение свойства можно лишь прочитать; попытка присвоить свойству AProperty значение вызовет ошибку компиляции.

Для присвоения свойству значения по умолчанию используется ключевое слово default:

property Visible: boolean read FVisible write SetVisible default True;

Это означает, что при запуске программы начальное значение свойства будет установлено компилятором равным True.

Свойство может быть и векторным; в этом случае оно внешне выглядит как массив:

Property APoints[Index: Integer]: TPoint read GetPoint write SetPoint;

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

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

function GetPoint(Index: Integer): TPoint;

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

procedure SetPoint(Index: Integer; NewPoint: TPoint);

У векторных свойств есть еще одна важная особенность. Некоторые важные классы в Delphi (списки TList, наборы строк TStrings) " построены" вокруг основного векторного свойства. Один из методов такого класса дает доступ к некоторому массиву, а все остальные методы являются как бы вспомогательными. Специально для облегчения работы в этом случае векторное свойство может быть описано с ключевым словом default:

tyре TMyObject = class;

property Strings[Index: Integer]: string read Get write Put; default;

end;

Если у объекта есть default-свойство, то можно его не упоминать, а ставить индекс в квадратных скобках сразу после имени объекта:

var

AMyObject: TMyObject;

begin

AMyObject.Strings[1]: = 'First';

{первый способ}

AMyObject[2]: = 'Second'; {второй способ}

end.

Будьте внимательны, применяя зарезервированное слово default — как мы увидели, для обычных и векторных свойств оно употребляется в разных случаях и с разным синтаксисом.

О роли свойств в Delphi красноречиво говорит следующий факт: у всех имеющихся в распоряжении программиста стандартных классов 100% полей недоступны и заменены базирующимися на них свойствами. Рекомендуем при разработке собственных классов придерживаться этого же правила.

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

Начнем с класса " карта". Каждая карта должна имеет масть (suit) и вес (weight). Также ее отличительной чертой служит то, открыта ли она на данный момент или нет (назовем это свойство Known):

TCard = class(TShape) private

FSuit: TSuit;

FWeight: TWeight;

FKnown: boolean-protected

function GetOrder: Integer;

function GetCardName: string;

procedure SetKnown(bk: boolean);

public

constructor Create(AOwner: TComponent;

S: TSuit;

W: TWeight);

property Suit: TSuit read FSuit write FSuit;

property Weight: TWeight read FWeight write FWeight;

property Order: Integer read GetOrder;

property CardName: string read GetCardName;

property Known: boolean read FKnown write SetKnown;

end;

Помимо уже указанных свойств, нам понадобится порядковый номер карты в колоде (свойство order). Оно является производным от suit и weight"; карты упорядочены следующим образом — сначала все пики, затем трефы, бубны и, наконец, червы.

Const CardsInSuit = 8;

//в большинстве игр 8 карт в масти

function TCard.GetOrder: Integer;

begin Result: =(Integer(Suit)-1)*CardsInSuit+Integer(Weight);

end;

Также нам понадобится свойство CardName — имя карты, отображаемое при ее рисовании, например, “8 ”:

const SuitName: array [TSuit] of string =

(' ', chr(170), chr(167), chr(168), chr(169)); //эти символы соответствуют мастям в шрифте 'Symbol'

WeightName: array [TWeight] of string = ('7', " 8', '9', '10', 'J', 'Q', 'К', 'А'); // имена карт по старшинству

function TCard.GetCardName: string;

begin Result : = WeightName[Weight]+SuitName[Suit];

end;

Большинство описанных свойств напрямую обращаются к соответствующим полям. Только установка свойства Known выполнена посредством процедуры SetKnown — при открытии или закрытии карты ее нужно, как минимум, перерисовать. Этот метод, как и многие другие, будет добавлен в класс TCard впоследствии.

Свойства-массивы

Объявления свойств-массивов

Объявление свойств-массивов имеет ряд особенностей как при их объявлении, так и при использовании. Синтаксис объявления свойства-массива класса:

Property < имя cвoucmвa> [[Const] < индекс1>: < тип идекса>
[; [Coast] < индекс2>: < тип индекса> ] ]: < тип данных>
[Read < метод чтения> ]
[Write < метод записи> ][; Default];

Примечания:
• Индексы свойств-массивов оформляются аналогично параметрам процедур и функции, но только в квадратных скобках.
• Идентификатор индекса свойства-массива может дополняться спецификаторомConst.
• Команда Default отделяется от других команд объявления свойства-массива точкой с запятой и должна быть последней.
• Свойства-массивы могут иметь индексы любого стандартного типа.

Доступ к полю типа массив с помощью свойства со строковым индексом производится по значению строки. Фактически ищется индекс элемента массива, значение которого равно строковому индексу свойства-массива.

Переопределение свойств при наследовании

При наследовании свойств можно в классе-потомке заменить унаследованную или добавить отсутствующую команду, изменить видимость и даже повторно объявить свойство.

а) Для изменения области видимости необходимо в классе-потомке объявить имя свойства без указания типа и команд в разделе с большей видимостью, обычно вPublic или Published, т.е. в сторону увеличения видимости.

Public Property MyProperty;

б) Для изменения или добавления команды следует объявить свойство без указания типа с дополнительной и/или измененной командой
(Read] Write|Stored|Default|NoDefault). Одновременно можно изменить и область видимости.

в) Для повторного объявления свойства следует указать имя свойства вместе с новым типом. Обязательно должна быть хотя бы одна команда доступа (Read|Write). При переопределении свойства можно вместо старых методов доступа объявить и использовать новые.

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

Некоторые свойства могут использовать не одно, а множество значений. Например, таковым является свойство Lines компонента ТМето, значением которого является множество строк. Обращение к конкретному значению свойства-массива осуществляется по индексу:

Memol.Lines[0]: = 'Строка';

Свойства-массивы имеют перечисленные ниже особенности.

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

• Доступ к значению свойства-массива может быть только косвенным (через методы SetXXXX/GetXXXX).

• Если в определении свойства-массива используется несколько индексов (многомерные свойства-массивы), методы доступа должны содержать параметры для каждого индекса в том же порядке, что и в объявлении свойства.

• Свойства-массивы нельзя объявлять в секции published. Доступ к значениям свойства-массива на этапе конструирования возможен только с помощью специализированного редактора свойств.

В качестве примера создадим класс TPlanets, который хранит названия всех планет Солнечной системы (листинг 7.1).

Листинг 7.1. Класс TPlanets, иллюстрирующий особенности свойств-массивов Unit Planets;

interface

uses

Classes, SysUtils; type

TPlanets = class(TComponent) private

// Методы доступа к свойству-массиву

function GetPlan^tName(const AIndex: Integer): String; function GetPlanetPosition(const AName: String): Integer; public

// Свойства-массивы нельзя публиковать!

// Следующее свойство возвращает название планеты по ее номеру property PlanetName[const AIndex: Integer]: String

read GetPlanetName; default; // Следующее свойство возвращает номер планеты по ее названию property PlanetPosition[const AName: String]: Integer

read GetPlanetPosition;

end; implementation const

PlanetNames: array [1..9] of String[8] = ('Меркурий1, 'Венера1, 'Земля', 'Марс', 'Юпитер', 'Сатурн', 'Нептун', 'Уран', 'Плутон');

продолжение &

Листинг 7.1 (продолжение)

function TPlanets.GetPlanetName(const AIndex: Integer): String; // Возвращает название планеты по ее номеру. Если номер выходит // за пределы диапазона, возбуждает исключение. begin

if Aindex in [1..9] then

Result: = PlanetNames[AIndex] else

raise Exception.Create('Ошибочный номер')

end;

function TPlanets.GetPlanetPosition(const AName: String): Integer; // Возвращает номер планеты по ее имени. // Если имя неверно, возвращает 0.

к: Integer; begin к: = 0; repeat

inc(к);

until (к = 10) or (CompareStr(AnsiUpperCase(AName),

AnsiUpperCase(PlanetNames[к]) = 0); if к < 10 then

Result: = к else

Result: = 0

end; end.

Обратите внимание: свойство-массив GetPlanetName определено с директивой default. Такое определение делает свойство-массив умалчиваемым и позволяет опускать название свойства при обращении к нужному значению. Два следующих оператора выводят одно и то же название:

ShowMessage(Planets.PlanetName[3]); ShowMessage(Planets[3]);

В классе можно сделать умалчиваемым только одно свойство-массив.

 

Индексированные свойства

 

Индексированные свойства определяются с помощью зарезервированного слова index. Например:

type

TSampleCalendar = plass(TCustomGrid) public

property Day: Integer index 3 read GetDateElement write SetDateElement;

property Month: Integer index 2 read GetDateElement write SetDateElement;

property Year: Integer index 1 read GetDateElement write SetDateElement; private

FDate: TDateTime;

function GetDateElement(Index: Integer): Integer; procedure SetDateElement(Index: Integer; Value: Integers-end;

Как видите, все три индексированные свойства используют одинаковые методы доступа, но передают им разные индексы. Это позволяет методам определить, о каком элементе даты идет речь, и произвести соответствующие действия:

function TSampleCalendar.GetDateElement(Index: Integer): Integer; var

AYear, AMonth, ADay: Word; begin

DecodeDate(FDate, AYear, AMonth, ADay);

case Index of 1: Result: = AYear; 2: Result: = AMonth; 3: Result: = ADay;

else

Result: = -1

end end;

procedure TSampleCalendar.SetDateElement(Index: Integer;

Value: Integer); * var

AYear, AMonth, ADay: Word; begin

if Value > 0 then begin

DecodeDate(FDate, AYear, AMonth, ADay); case Index of 1: AYear: = Value; 2: AMonth: = Value; 3: ADay: = Value; else Exit end;

FDate: = EncodeDate(AYear, AMonth, ADay); Refresh end end;

Индексированными можно объявлять только свойства одного и того же типа. Они позволяют экономить память: если бы свойства не были индексированы, нам пришлось бы писать пару почти одинаковых методов GetXXXX/SetXXXX для каждого свойства.

 

Свойства и иерархия классов

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

Принцип наследования приводит к созданию дерева классов, постепенно разрастающегося при перемещении отTObject к его потомкам. Каждый потомок дополняет возможности своего родителя новыми свойствами и передает их своим потомкам. В состав Delphi входят более 300 различных классов. Для примера приведем небольшой фрагмент дерева классов Delphi:


Рис.1. Фрагмент дерева классов Delphi

Класс TPersistent обогащает возможности своего родителя TObject; он умеет сохранять данные в файле и получать данные из него. Класс TComponent умеет взаимодействовать со средой разработчика и передает это умение своим потомкам. Класс TComponent является базовым для создания не визуальных компонентов. TControl не только способен работать с файлами и средой разработчика, но он уже умеет создавать и обслуживать видимые на экране изображения. Класс TControl является базовым для создания визуальных компонентов. Потомок этого классаTWinControl может создавать Windows-окна т.д.

Свойства классов в Delphi

Поля данных целесообразно защитить от несанкционированного доступа по принципу инкапсуляции. Доступ к полям реализуется через свойства, в которые входят метод чтения, и метод записи полей. Исходя из этого, поля объявляются в разделе private (закрытый раздел класса). Для реализации доступа потомков данного класса иногда лучше объявить поля в защищенном разделе – protected. Важно: в большинстве случаев идентификаторы полей могут совпадать с именами соответствующих свойств, где добавляется префикс (символ) – «F».
Синтаксис:
Property< имя свойства>: < тип> read< имя поля или метода чтения>
write< имя поля или метода чтения>
< директивы запоминания>;
При прямом чтении или записи данных в разделе read или write записывается имя поля. По умолчанию директива запоминания имеет вид: default< сохранять значение по умолчанию, если не изменено пользователем>. Которое не задает начальные условия.
Запись имени метода в разделы read и write:

1. Read. Используется чаще всего функция чтения без параметра, которая возвращает значение типа, объявленного для класса. Префикс Get.

2. Write. Используется, скорее всего, процедура записи с одним параметром типа. Префикс Set, после которого пишется имя свойства.


Поделиться:



Популярное:

Последнее изменение этой страницы: 2016-07-14; Просмотров: 585; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.049 с.)
Главная | Случайная страница | Обратная связь