Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Инкапсуляция при помощи свойств
Как правило, объект — это сложная конструкция, свойства и поведение составных частей которой находятся во взаимодействии. К примеру, если мы моделируем взлетающий самолет, то после набора им определенной скорости и отрыва от земли принципы управления им полностью изменяются. Поэтому в объекте " самолет" явно недостаточно просто увеличить значение поля " скорость" на несколько километров в час — такое изменение должно автоматически повлечь за собой целый ряд других. Классическое правило объектно-ориентированного программирования утверждает, что для обеспечения надежности нежелателен прямой доступ к полям объекта: чтение и обновление их содержимого должно производиться посредством вызова соответствующих методов. Это правило и называется инкапсуляцией. В старых реализациях ООП (например, в 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>: < тип идекса> Примечания: Доступ к полю типа массив с помощью свойства со строковым индексом производится по значению строки. Фактически ищется индекс элемента массива, значение которого равно строковому индексу свойства-массива. Переопределение свойств при наследовании При наследовании свойств можно в классе-потомке заменить унаследованную или добавить отсутствующую команду, изменить видимость и даже повторно объявить свойство. а) Для изменения области видимости необходимо в классе-потомке объявить имя свойства без указания типа и команд в разделе с большей видимостью, обычно вPublic или Published, т.е. в сторону увеличения видимости. Public Property MyProperty; б) Для изменения или добавления команды следует объявить свойство без указания типа с дополнительной и/или измененной командой в) Для повторного объявления свойства следует указать имя свойства вместе с новым типом. Обязательно должна быть хотя бы одна команда доступа (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: Класс TPersistent обогащает возможности своего родителя TObject; он умеет сохранять данные в файле и получать данные из него. Класс TComponent умеет взаимодействовать со средой разработчика и передает это умение своим потомкам. Класс TComponent является базовым для создания не визуальных компонентов. TControl не только способен работать с файлами и средой разработчика, но он уже умеет создавать и обслуживать видимые на экране изображения. Класс TControl является базовым для создания визуальных компонентов. Потомок этого классаTWinControl может создавать Windows-окна т.д. Свойства классов в Delphi Поля данных целесообразно защитить от несанкционированного доступа по принципу инкапсуляции. Доступ к полям реализуется через свойства, в которые входят метод чтения, и метод записи полей. Исходя из этого, поля объявляются в разделе private (закрытый раздел класса). Для реализации доступа потомков данного класса иногда лучше объявить поля в защищенном разделе – protected. Важно: в большинстве случаев идентификаторы полей могут совпадать с именами соответствующих свойств, где добавляется префикс (символ) – «F». 1. Read. Используется чаще всего функция чтения без параметра, которая возвращает значение типа, объявленного для класса. Префикс Get. 2. Write. Используется, скорее всего, процедура записи с одним параметром типа. Префикс Set, после которого пишется имя свойства. Популярное:
|
Последнее изменение этой страницы: 2016-07-14; Просмотров: 628; Нарушение авторского права страницы