Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Ревизия интерфейсов (статья)

01.01.2007

Ревизия интерфейсов.

Часть 1: Объявления, реализация и разрешение имен методов

Я впервые написал об интерфейсах в апрельском выпуске Delphi Informant Magazine за 1998 год. Почти за два года произошло несколько важных событий. Первое - в Delphi 4 предоставлен новый и важный способ реализации интерфейса в классе. Второе, и возможно более важное - интерфейсы быстро выходят за рамки своего первоначального назначения: естественной поддержки COM (Компонентная Объектная Модель фирмы Майкрософт). В результате, видимо, пришло время для пересмотра этой важной темы.

Но только, что является интерфейсом и почему он так важен? Интерфейс - это определение методов и свойств, которые могут быть реализованы классом. Интерфейс предусматривает совместимость по назначению (часто называемую полиморфизмом) между различными объектами, которые реализуют общий интерфейс. Еще важнее, что интерфейсы обеспечивают полиморфизм без ограничений, обычно связываемых с полиморфизмом через наследование. Действительно, хотя два класса не наследуют методы от общего предка, к ним можно обращаться полиморфно в отношении метода, если этот метод определен для интерфейса обоих классов реализации. С другой стороны, интерфейсы позволяют определять свои методы и свойства абсолютно независимо от объекта реализации.

Эта статья начинается с обзора, почему интерфейсы важны в объектном Паскале, и продолжается детальным обсуждением характеристик интерфейсов. Это обсуждение включает декларацию интерфейсов, требования к их реализации, и разрешение имен методов. Во второй части этой статьи будет продолжение с подробным примером использования интерфейсов, включая использование интерфейса делегированием, предоставленного в Delphi 4.

Почему в Delphi добавлены интерфейсы?

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

Эту концепцию трудно описать, но пример поможет ее проиллюстрировать. Рассмотрим ситуацию, когда Вы хотите создать группу компонент пользовательского интерфейса (UI), которые разделяют общие способности, такие как способность загрузки строковых данных из файла ресурсов. Кроме того, Вы можете захотеть предоставить новый метод, названный LoadStrings, в каждом из Ваших новых UI компонентов, и в реализации этого метода загрузить строки этих компонент из ресурсного файла, возможно основываясь на свойстве компонента Tag.

Пока все хорошо. Однако, другой аспект этой структуры - возможность инициирования процесса загрузки для каждого из Ваших компонент общей командой. В Delphi 1 и 2, где интерфейсы еще не были предоставлены, это было почти невозможно. Рассмотрим псевдокод на рисунке 1 (который будет компилироваться).

// Псевдокод.
interface
 
uses controls, stdctrls;
type
TNewButton = class(TButton)
public
procedure LoadStrings; virtual;
end;
TNewLabel = class(TLabel)
public
procedure LoadStrings; virtual;
end;
implementation
 
procedure TNewLabel.LoadStrings;
begin
// Код для загрузки строк NewLabel
// из файла ресурсов.
end;
 
procedure TNewButton.LoadStrings;
begin
// Код для загрузки строк NewButton
// из файла ресурсов.
end;

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

procedure DoLoadStrings(LoadableObject: TControl);
begin
  LoadableObject.LoadStrings;
end;

Проблема в том, что хотя LoadStrings может быть законным методом определенного экземпляра TControl, к которому Вы применяете DoLoadStrings, это не метод класса TControl. Только, если LoadStrings будет публичным или опубликованным методом класса TControl, этот код будет правильно компилироваться.

Подход со множественным наследованием

Если бы Delphi поддерживала множественное наследование, эта проблема была бы решена довольно легко. Действительно, Вы могли бы создать другой класс (назовем его TLoadable) который включал бы абстрактный виртуальный метод LoadStrings. Тогда, при объявлении каждого из Ваших UI компонент, Вы могли бы объявить их наследниками от обоих естественных предков (TNewButton от TButton и TNewLabel от TLabel) и TLoadable. Псевдокод, показанный на рисунке 2 демонстрирует как это могло бы выглядеть.

// Псевдокод.
interface
 
uses controls, stdctrls;
 
type
TLoadable = class(TObject)
public
procedure LoadStrings; virtual; abstract;
end;
TNewButton = class(TButton, TLoadable)
public
procedure LoadStrings; override;
end;
TNewLabel = class(TLabel, TLoadable)
public
procedure LoadStrings; override;
end;
 
implementation
 
procedure TNewLabel.LoadStrings;
begin
// Код для загрузки строк NewLabel
// из файла ресурсов.
end;
 
procedure TNewButton.LoadStrings;
begin
// Код для загрузки строк NewButton
// из файла ресурсов.
end;

Если бы Delphi поддерживал множественное наследование, Вы могли бы создать другой класс (TLoadable), который включал бы абстрактный виртуальный метод LoadStrings.

Теперь все, что Вы должны были бы сделать - изменить описание DoLoadStrings, чтобы оно было похоже на следующее:

procedure DoLoadStrings(LoadableObject: TLoadable);
begin
  LoadableObject.LoadStrings;
end;

Поскольку этот измененный метод получает объект TLoadable как параметр, и поскольку объекты TLoadable имеют видимый метод LoadStrings, все должно работать прекрасно. Единственная проблема состоит в том, что Delphi не поддерживает множественное наследование, и, следовательно, этот код нельзя откомпилировать.

Решение: Интерфейсы

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

Рассмотрим пример кода, показанный на рисунке 3. Этот код включает одно объявление интерфейса, названного ILoadable, и объявления двух классов. Каждый из этих классов реализует интерфейс ILoadable.

// Пример кода.
interface
 
uses stdctrls;
 
type
ILoadable = interface
procedure LoadStrings;
end;
TNewButton = class(TButton, ILoadable)
public
procedure LoadStrings;
end;
TNewLabel = class(TLabel, ILoadable)
public
procedure LoadStrings;
end;
 
implementation
 
procedure TNewLabel.LoadStrings;
begin
// Код для загрузки строк NewLabel
// из файла ресурсов.
end;
 
procedure TNewButton.LoadStrings;
begin
// Код для загрузки строк NewButton
// из файла ресурсов.
end;

Этот код включает одно объявление интерфейса и объявления двух классов.

На языке интерфейсов мы говорим, что оба класса TNewButton и TNewLabel реализуют интерфейс ILoadable. Кроме того, поскольку оба этих класса реализуют общий интерфейс, они совместимы по назначению с интерфейсной ссылкой. Поэтому, мы можем заставить эту структуру работать обобщенно, изменив тип параметра в нашем методе DoLoadStrings на ILoadable. Следующий сегмент кода демонстрирует как выглядит законченный метод:

procedure DoLoadStrings(LoadableObject: ILoadable);
begin
  LoadableObject.LoadStrings;
end;
 
Поскольку DoLoadStrings может получать ILoadable объект, следующий фрагмент кода полностью законен:
 
var
  NewButton1: TNewButton;
  NewLabel1: TNewLabel;
begin
  NewButton1 := TNewButton.Create(Application);
  DoLoadStrings(NewButton1);
  NewLabel1 := TNewLabel.Create(Application);
  DoLoadStrings(NewLabel1);

Декларация интерфейсов в объектном Паскале

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

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

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

Следующий пример - простая декларация интерфейса, которая содержит метод ShowMessage и свойство MessageText. Два других метода GetMessageText и SetMessageText, также рассматривается как методы интерфейса:

type
IShowMessage = interface
  function ShowMessage: Boolean;
  function GetMessageText: string;
  procedure SetMessageText(Value: string);
  property MessageText: string
  read GetMessageText write SetMessageText;
end;

Для сравнения, декларация класса может описывать поля членов в частях read и write. Это называется прямым доступом, потому что объект, читая свойство, будет читать непосредственно из поля, и любой объект, пишущий в свойство, будет писать непосредственно в поле.

Все методы в декларации интерфейса рассматриваются как виртуальные и абстрактные по определению. Следовательно, в декларации интерфейса никогда не используются директивы virtual и abstract. Кроме того, в декларации интерфейса нет идентификаторов видимости (public, published и т. д.). Со всеми методам и свойствами, объявленными в интерфейсе, обращаются как с public, хотя класс, реализующий методы, может использовать идентификаторы видимости для управления их видимостью в классе.

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

Реализация методов интерфейса

Класс, который реализует интерфейс, нужен, чтобы обеспечить реализацией каждый метод, объявленный в интерфейсе. Реализация может быть обеспечена или явным объявлением или в соответствии с наследованием от предка класса. Например, если класс TNewLabel объявлен для реализации интерфейса ILoadable и в этом интерфейсе объявлен один метод, названный LoadStrings, то класс TNewLabel должен или наследовать метод с соответствующим описанием по имени LoadStrings, или этот метод должен быть явно объявлен в классе и реализован. Кроме того, если унаследованный метод LoadStrings - абстрактный, то должна быть предусмотрена реализация этого метода в реализации класса TNewLabel.

Хотя класс, который реализует интерфейс, должен реализовать все методы, объявленные в этом интерфейсе, не требуется, чтобы он реализовал все свойства интерфейса, или хотя бы одно из этих свойств. Например, в интерфейсе IShowMessage, описанном ранее, класс, реализующий IShowMessage, не обязан иметь свойство MessageText.

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

Иерархия интерфейсов

Интерфейсы организованы в иерархию, очень похожую на иерархию классов Delphi. В Delphi, все интерфейсы, за исключением IUnknown, наследуются от существующего интерфейса. IUnknown- интерфейс самого высокого уровня, это значит, что все интерфейсы обязательно наследуются от него. Когда Вы видите объявление интерфейса, в котором не определен предок (подобно интерфейсу IShowMessage, объявленному в коде предыдущего примера), интерфейс наследуется от IUnknown. Интерфейсы, которые наследуются от другого интерфейса, чем IUnknown, содержат название интерфейса предка в своем объявлении типа. Это демонстрируется в следующем объявлении интерфейса:

type

INewInterface = interface(IDispatch)

...

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

 
IUnknown = interface
['{ 00000000-0000-0000-C000-000000000046 }']
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;

Зная, что IShowMessage наследуется от IUnknown, любой класс, который реализует IShowMessage, должен реализовать не только три метода IShowMessage, но также и три метода IUnknown. К счастью, как описано ранее в этой статье, эта реализация может быть унаследована. Например, класс TComponent реализует методы QueryInterface, _AddRef, and _Release. В результате, любой объект, который наследуется от TComponent, может реализовать IShowMessage, объявляя и реализуя только три метода IShowMessage. Реализация методов IUnknown обеспечивается наследованием.

Интерфейсные ссылки

Интерфейсная ссылка указывает на экземпляр объекта, который реализует интерфейс. Интерфейсная ссылка может быть переменной, формальным параметром, свойством объекта, и даже значением, возвращаемым из функции. В процедуре DoLoadStrings, описанной ранее, интерфейсная ссылка была формальным параметром (LoadableObject).

Интерфейсная ссылка используется для вызова методов интерфейса, а также для чтения и/или записи свойств интерфейса. Во время выполнения, вызывается конкретный метод (или метод доступа к свойству) объекта, на который указывает ссылка. Например, передача объекта TNewButton в DoLoadStrings, приводит к вызову конкретной реализации LoadStrings, определенной в классе TNewLabel. Для сравнения, передача TNewLabel в DoLoadStrings приводит к поведению, определенному в TNewLabel. Однако, интерфейсная ссылка ограничена работой только с теми свойствами и методами, которые определены в интерфейсе. К любым методам, свойствам или полям объекта, которые не являются частью определения интерфейса, нельзя обращаться через интерфейсную ссылку.

Снова обсудим метод DoLoadStrings. Хотя Вы можете передавать в этот метод любые объекты, реализующие ILoadable, Вы можете использовать формальный параметр LoadableObject только для вызова методов ILoadable. Действительно, даже притом, что Вы можете передавать экземпляр TNewButton в DoLoadStrings, компилятор не разрешит обращаться к свойству Enabled класса TNewButton из LoadableObject - или к любому другому свойству этого класса. Могут быть вызваны только методы LoadStrings, QueryInterface, _AddRef и _Release, потому что только они являются методами интерфейса ILoadable. (Помните, что ILoadable наследуется от IUnknown по определению).

Еще одно дополнение относительно интерфейсных ссылок состоит в правиле: Вы не можете преобразовывать интерфейсную ссылку в объект. Например, Вы не можете преобразовать LoadableObject в TNewButton.

Разрешение имен методов интерфейса

Как Вы уже знаете, любому объекту, который реализует интерфейс, необходимо реализовывать методы, определенные в этом интерфейсе. Это может составить проблему, если в интерфейсе объявлен метод, чье имя уже используется объектом, который должен реализовать интерфейс.

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

Delphi обеспечивает отображение методов интерфейса на реализующие методы с другим именем. Например, рассмотрим следующее объявление TMessageObject:

type
TMessageObject = class(TParentMessageObject,IShowMessage)
 FMessText: string;
 function IShowMessage.ShowMessage := DisplayMessage;
 function GetMessagText: string;
 procedure SetMessageText(Value: string);
 function DisplayMessage: Boolean;
end;

Это объявление описывает, что TMessageObject наследуется от TParentMessageObject, и реализует IShowMessage. Поскольку TMessageObject уже имеет метод ShowMessage (наследованием), есть необходимость отобразить метод IShowMessage.ShowMessage на метод с другим именем, которое в этом случае является DisplayMessage. При использовании разрешения метода интерфейса, чтобы отобразить метод интерфейса на новое имя метода - метод интерфейса и метод на который он отображается должны иметь одинаковый список параметров и одинаковое возвращаемое значение.

Учитывая предшествующее объявление типа, если вызывается метод ShowMessage экземпляра TMessageObject, с использованием объектной ссылки с типом TMessageObject, выполнится наследуемый метод ShowMessage. Однако, если экземпляр TMessageObject назначен переменной IShowMessage, и вызывается метод ShowMessage, выполнится метод DisplayMessage.

Заключение

Обсуждение интерфейсов будет продолжено во второй части этой серии. Вторая часть начнется с рассмотрения того, как сильно отличается использование объектов с интерфейсными ссылками от стандартного использования объектов. Вы также найдете детальный обзор реализации интерфейсов делегированием, представленным в Delphi 4.

Cary Jensen is President of Jensen Data Systems, Inc., a Houston-based database development company. He is co-author of 17 books, including Oracle JDeveloper [Oracle Press, 1998], JBuilder Essentials [Osborne/McGraw-Hill, 1998], and Delphi in Depth [Osborne/McGraw-Hill, 1996]. He is a Contributing Editor of Delphi Informant Magazine, and an internationally respected trainer of Delphi and Java. For more information, visit http://www.delphizine.com/include/Click_Redir.asp?Url=http://www.jensendatasystems.com/, or e-mail Cary at mailto:cjensen@compuserve.com.

By Cary Jensen, Ph.D.

Перевел Мотов Олег mailto:olegm@tebuk.parma.ru http://olegmotov.h1.ru

Ревизия интерфейсов

Часть 2: Интерфейсные ссылки против объектных ссылок

Как Вы помните из первого выпуска этой серии статей, интерфейсы - это объявления методов и свойств, которые могут быть реализованы в классе.

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

В первой части я описал основные правила декларации интерфейсов и реализации классов. В этой части мы рассмотрим использование объектов через интерфейсные ссылки, и покажем как это использование отличается от обычного использования объектов. Я также опишу дополнительную поддержку реализации интерфейсов, которая была добавлена в Delphi 4. Эта поддержка, называемая реализация интерфейсов делегированием, завершает модель интерфейсов Delphi. Это статья заканчивается обсуждением двух типов реализации интерфейсов делегированием, где я докажу, что только одна форма реализации интерфейсов делегированием безопасна в использовании, тогда как другой свойственна опасность.

Интерфейсные ссылки против объектных ссылок

В использовании интерфейсных и объектных ссылок есть существенные различия. Самое большое связано с управлением жизненным циклом объектов. Действительно, обращение к объектам через интерфейс происходит с подсчетом ссылок. Когда Вы присваиваете объект интерфейсной ссылке, неявно вызывается метод _AddRef. Точно так же, когда интерфейсная ссылка освобождается, вызывается метод объекта _Release.

Все объекты, реализующие интерфейс, должны реализовывать методы _AddRef и _Release. Это необходимо, потому что все интерфейсы в конечном счете наследуются от IUnknown, и (как обсуждалось в первой части), любой объект, который реализует интерфейс, должен также реализовать все методы, объявленные, в предках интерфейса. В реализации метода _AddRef, объект должен увеличить внутренний счетчик ссылок. В методе _Release, этот счетчик ссылок уменьшается. Кроме того, в методе _Release объект должен явно уничтожаться, если счетчик ссылок уменьшится до нуля. Пример реализации такого типа показан на методах интерфейса IUnknown, которые реализованы в классе TInterfacedObject, показан на рисунке 1.

function TInterfacedObject.QueryInterface(const IID: TGUID; out Obj): HResult;const E_NOINTERFACE = HResult($80004002);
begin
 if GetInterface(IID, Obj) then
   Result := 0
 else
  Result := E_NOINTERFACE;
end;
 
function TInterfacedObject._AddRef: Integer;
begin
 Result := InterlockedIncrement(FRefCount);
end;
 
function TInterfacedObject._Release: Integer;
begin
 Result := InterlockedDecrement(FRefCount);
 if Result = 0 then Destroy;
end;

Рисунок 1: Методы IUnknown, реализованные классом TInterfacedObject.

Компилятор автоматически генерирует необходимый вызов QueryInterface при назначении объекта, реализующего интерфейс, интерфейсной ссылке. Как показано на рисунке 1, TInterfacedObject.QueryInterface вызывает GetInterface, который является методом класса TObject. GetInterface вызывает метод _AddRef объекта, у которого запрашивается интерфейс.

Компилятор также генерирует вызовы _Release. Есть три ситуации, в которых компилятор генерирует обращения к _Release:

·Интерфейсной ссылке присваивается значение nil.
·Интерфейсной ссылке переназначается другой объект, реализующий интерфейс.
·Интерфейсная ссылка выходит из области видимости.
Как Вы можете видеть на рисунке 1, когда вызывается метод _Release и счетчик ссылок уменьшается до нуля, автоматически вызывается деструктор объекта.

Такое поведение, которое происходит только при использовании интерфейсных ссылок, существенно отличается от поведения, наблюдаемого при использовании объектных ссылок. При использовании объектных ссылок, жизненный цикл объектов управляется одним из двух способов: или Вы полагаетесь на сборку "мусора" класса TComponent, в котором Владелец(Owner) объекта (назначенный в методе TComonent.Create) уничтожает все существующие объекты, которыми он владеет (то есть те объекты, на которые ссылается его свойство-массив Components) как часть своего собственного процесса уничтожения; или Вы явно вызываете метод Free (или Release для экземпляра TForm). Если объектная ссылка не потомок TComponent (или если у объекта нет собственника пр. пер.), то Вы должны явно вызвать метод Free для освобождения объекта.

Различия в управлении жизненным циклом приводят к существенно разному коду, в зависимости от того, используете ли Вы интерфейсные или объектные ссылки. Эти различия можно найти в примере приложения DEMOINT (доступного для загрузки, для подробностей смотрите конец статьи). Возможно Вы захотите загрузить этот файл и рассмотреть весь модуль главной формы. Для краткости, я в данное время сосредоточусь только на части этого файла.

Сначала рассмотрим декларацию одного интерфейса и двух классов, которые реализуют этот интерфейс. Для ясности, я сохранил декларацию этого интерфейса и классы его реализации очень простой, как показано на Рисунке 2.

type
IShowMessage = interface(IUnknown)
['{ F8AA90A1-EA2D-11D0-82A8-444553540000 }']
function ShowMessage:Boolean;
function GetMessageText: string;
procedure SetMessageText(Value: string);
property MessageText: string
read getMessageText write setMessageText;
end;
 
TYesDefault = class(TInterfacedObject, IShowMessage)
FMessageText: string;
function ShowMessage: Boolean;
function GetMessageText: string;
procedure SetMessageText(value: string);
public
destructor Destroy; override;
end;
 
TNoDefault = class(TInterfacedObject, IShowMessage)
FMessageText: string;
function ShowMessage: Boolean;
function GetMessageText: string;
procedure SetMessageText(value: string);
public
destructor Destroy; override;
end;

Рисунок 2: Декларация интерфейса и двух классов, которые его реализуют.

Первая вещь, на которую Вы обратите внимание, состоит в том, что интерфейс IShowMessage связан с большим числом. На это число ссылаются как на глобальный уникальный идентификатор интерфейса, или сокращенно GUID (произносится как гуид). Это 128-битный число, сгенерированное вызовом API-функции Windows, которая гарантирует, что это число абсолютно уникально, даже среди различных компьютеров. Цель GUID - уникально идентифицировать интерфейс, когда он зарегистрирован для использования COM (объектная модель программных компонентов) в системном реестре Windows.

Object Pascal не требует, чтобы Вы генерировали и регистрировали GUID для интерфейсов, которые вы создаете. Однако, в том случае, если Вы решите использовать интерфейс как интерфейс COM когда-нибудь в будущем, не повредит назначить интерфейсу GUID. И редактор Delphi делает генерацию GUID очень легким. Чтобы вставить GUID в исходный код, просто нажмите [Ctrl][Shift][G].

Остальная часть декларации интерфейса довольно проста. В ней объявляется одно свойство, названное MessageText, и три метода. Два из этих методов - методы доступа к свойству.

Как Вы знаете из первой части этой серии, интерфейсы не реализуют методы. Это ответственность классов, которые реализуют интерфейс. На рисунке 2, классы TYesDefault и TNoDefault реализуют интерфейс IShowMessage. Поскольку ни TYesDefault ни TNoDefault не наследуют методы интерфейса IShowMessage, в этих классах должны быть явно объявлены и реализованы эти методы. Обратите внимание, что свойство MessageText не объявлено ни в TYesDefault ни в TNoDefault. Как упоминалось в первой части, классы, реализующие интерфейс, не требуют явного объявления свойств интерфейса. Это действие абсолютно необязательно, и я посчитал его необязательным и в нашем случае. Однако, поскольку эти классы должны реализовать методы доступа к свойству GetMessageText и SetMessageText, и эти методы требуют сохранения текста сообщения, было необходимым объявить поле, названное FMessageText, чтобы хранить содержимое сообщения в объектах класса.

Хотя эти два класса выглядят почти одинаково, они различны. Различие можно найти в реализациях метода ShowMessage. Рисунок 3 показывает реализацию этих классов. Я также перекрыл деструкторы этих классов. Это было сделано для того, чтобы можно было увидеть результат управления жизненным циклом и сравнить как оно отличается между интерфейсными и объектными ссылками.

function TYesDefault.GetMessageText: string;
begin
Result := FMessageText;
end;
 
procedure TYesDefault.SetMessageText(value: string);
begin
FMessageText := Value;
end;
 
function TYesDefault.ShowMessage;
begin
if MessageBox(Application.Handle, 'Continue?',
PChar(FMessageText),
MB_OKCANCEL+MB_DEFBUTTON1) <> IDOK then
Result := False
else
Result := True;
end;
 
destructor TYesDefault.Destroy;
begin
Dialogs.ShowMessage('YesDefault: Goodbye');
inherited;
end;
 
function TNoDefault.GetMessageText: string;
begin
Result := FMessageText;
end;
 
procedure TNoDefault.SetMessageText(value: string);
begin
FMessageText := Value;
end;
 
function TNoDefault.ShowMessage;
begin
if MessageBox(Application.Handle, 'Continue?',
PChar(FMessageText),
MB_OKCANCEL+MB_DEFBUTTON2) <> IDOK then
Result := False
else
Result := True;
end;
 
destructor TNoDefault.Destroy;
begin
Dialogs.ShowMessage('NoDefault: Goodbye');
inherited;
end;

Рисунок 3: Реализация TYesDefault и TNoDefault.

Как упоминалось ранее, единственное реальное различие между реализациями этих классов может быть обнаружено в методе ShowMessage. Когда вызывается метод TYesDefault.ShowMessage, отображается диалоговое окно с заданной по умолчанию кнопкой OK. Для сравнения, когда вызывается TNoDefault.ShowMessage, заданная по умолчанию - кнопка Cancel. Это единственное различие позволяет нам показать, какая из реализаций интерфейса вызывается, отметив, которая из кнопок на отображенном диалоговом окне является основной.

Давайте теперь обратим наше внимание на использование этих двух объектов. Основная форма DEMOINT содержит четыре кнопки (см. рисунок 4). Начнем с первых двух - помеченных Use Objects и Use Interfaces. На рисунке 5 - обработчик события OnClick, связанный с кнопкой Use Objects.

Основная форма проекта DEMOINT.

procedure TForm1.Button1Click(Sender: TObject);
var
YesDefault1: TYesDefault;
NoDefault1: TNoDefault;
begin
YesDefault1 := TYesDefault.Create;
NoDefault1 := TNoDefault.Create;
YesDefault1.SetMessageText('This is a TYesDefault');
YesDefault1.ShowMessage;
NoDefault1.SetMessageText('This is a TNoDefault');
NoDefault1.ShowMessage;
YesDefault1.Free;
NoDefault1.Free;
end;

Рисунок 5: Обработчик события OnClick для кнопки Use Objects.

Здесь мы видим традиционный способ использования экземпляров класса. Вызывается конструктор класса, вызываются методы экземпляра класса, и затем он освобождается. Обратите внимание, что мы используем методы доступа, чтобы установить текст сообщения диалогового окна. Мы не можем использовать свойство MessageText в этом случае, потому что оно не является свойством классов TYesDefault или TNoDefault.

Давайте сравним этот код с кодом, связанным с кнопкой помеченной Use Interfaces. На рисунке 6 показан код обработчика события OnClick, связанного с этой кнопкой.

procedure TForm1.Button2Click(Sender: TObject);
var
IntVar: IShowMessage;
begin
IntVar := TYesDefault.Create;
IntVar.MessageText := 'This is a TYesDefault';
IntVar.ShowMessage;
IntVar := TNoDefault.Create;
IntVar.MessageText := 'This is a TNoDefault';
IntVar.ShowMessage;
end;

Рисунок 6: Обработчик события OnClick для кнопки Use Interfaces.

Здесь мы используем переменную типа IShowMessage - интерфейсную ссылку. Кроме того, мы можем использовать свойство MessageText, чтобы установить текст сообщения диалогового окна, потому что у интерфейса IShowMessage есть это свойство. Кроме этих двух различий, есть два других важных различия между этой и предыдущей частями кода. Первое - для ссылки на два экземпляра объектов используется одна переменная типа IShowMessage. Другими словами, экземпляры классов TYesDefault и TNoDefault совместимы по назначению со ссылкой IShowMessage.

Второе важное отличие состоит в том, что эти объекты не освобождаются явно. Вместо этого, их жизненный цикл управляется интерфейсной ссылкой. Вы это увидите, если загрузите и запустите этот проект. Когда Вы нажмете на кнопку Use Interfaces, вначале увидите диалоговое окно YesDefault. Как только Вы нажмете на одну из кнопок диалогового окна, Вы увидите сообщение, показанное деструктором экземпляра YesDefault. Этот деструктор вызывается когда переменной IShowMessage назначается экземпляр TNoDefault. Другими словами, когда интерфейсная ссылка направляется на другой объект, реализующий интерфейс, первая ссылка освобождается и в результате этого вызывается деструктор. После сообщения деструктора, показывается диалоговое окно экземпляра NoDefault. Снова, после нажатия на кнопку этого диалогового окна, Вы увидите сообщение, показанное деструктором объекта NoDefault. В данном случае, это происходит, потому что переменная IShowMessage вышла из области видимости.

Реализация интерфейсов делегированием

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

На первый взгляд это дополнение поддержки интерфейсов может показаться необязательным, но в действительности ("в действительности" устаревшее выражение, на самом деле современные люди употребляют выражение "на самом деле" пр. пер.) оно очень полезно. На примере продемонстрируем почему. Представим, что у Вас есть 20 различных объектов, которые должны реализовать один интерфейс. Допустим, что эти объекты не наследуют методы интерфейса. В Delphi 3 было необходимо для каждого из реализующих классов объявить и реализовать методы интерфейса. Допустим, что все 20 классов, реализуют интерфейс абсолютно одинаково и Вы имеете 20 различных копий одного кода. Если Вы решите, что общая реализация должна быть изменена, Вам надо будет изменить код в 20 различных классах.

Реализация интерфейсов делегированием решает эту проблему. Вместо объявления и реализации интерфейса в каждом из 20 классов, Вы можете создать один класс, реализующий интерфейс. Затем, в каждом из 20 классов, в которых должен быть реализован одинаковый интерфейс, Вы объявляете свойство, которое может содержать ссылку на этот предварительно созданный класс. Другими словами, все 20 классов могут совместно использовать реализацию, предоставленную одним классом. Если Вам позже понадобиться изменить реализацию интерфейса, Вы просто вернетесь к Вашему единственному классу и измените его реализацию. Поскольку все 20 классов делегируют реализацию этому единственному классу, их реализация автоматически изменится.

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

Проект DEMOINT содержит два класса, которые реализуют интерфейс IShowMessage делегированием. Первый класс - TClassProperty реализует интерфейс, используя свойство типа класс. Второй, названный TInterfaceProperty, реализует интерфейс IShowMessage, используя свойство интерфейс. Код на Рисунке 7 показывает декларацию этих двух классов.

type
TClassProperty = class(TComponent, IShowMessage)
private
FMyMessage: TYesDefault;
public
property MyMessage: TYesDefault
read FMyMessage write FMyMessage
implements IShowMessage;
end;
 
TInterfaceProperty = class(TComponent, IShowMessage)
private
FMyMessage: IShowMessage;
public
property MyMessage: IShowMessage
read FMyMessage write FMyMessage
implements IShowMessage;
end;

Рисунок 7: Классы TClassProperty и TInterfaceProperty.

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

Оба объявления классов также включают поле члена класса. Это поле используется для хранения ссылки на объект, назначаемый связанному свойству с использованием прямого доступа. В случае свойства TClassProperty.MyMessage, это поле имеет тип класс. Для сравнения, TInterfaceProperty.MyMessage использует поле типа IShowMessage.

Использование этих классов демонстрируется кнопками на основной форме DEMOINT помеченными как Object with class type interface property и Object with interface type interface property соответственно. На рисунке 8 показан код, связанный с обработчиками события этих кнопок.

procedure TForm1.Button3Click(Sender: TObject);
var
IntVar: IShowMessage;
ClassProperty1: TClassProperty;
begin
ClassProperty1 := TClassProperty.Create(Self);
ClassProperty1.MyMessage := TYesDefault.Create;
IntVar := ClassProperty1;
IntVar.MessageText := 'Message Text';
IntVar.ShowMessage;
ShowMessage('About to free the interface');
IntVar := nil;
ShowMessage('Interface freed. About to release the object');
ClassProperty1.Free;
ShowMessage('Object freed');
end;
 
procedure TForm1.Button4Click(Sender: TObject);
var
IntVar: IShowMessage;
InterfaceProperty1: TInterfaceProperty;
begin
InterfaceProperty1 := TInterfaceProperty.Create(Self);
InterfaceProperty1.MyMessage := TNoDefault.Create;
IntVar := InterfaceProperty1;
IntVar.MessageText := 'Message Text';
IntVar.ShowMessage;
ShowMessage('About to free the interface');
IntVar := nil;
ShowMessage('Interface freed. About to release the object');
InterfaceProperty1.Free;
ShowMessage('Object freed');
end;

Рисунок 8: Обработчики события OnClick кнопок для интерфейса с типом класс и интерфейса с типом интерфейс.

Для обоих обработчиков событий, первая строка создает экземпляр объекта, который реализует интерфейс делегированием, а вторая строка назначает объект интерфейсному свойству. В действительности, объект, который реализует интерфейс, вероятно был бы назначен интерфейсному свойству в конструкторе класса, который реализует интерфейс делегированием. Например, было бы правильно, и возможно более естественно, назначить экземпляр TYesDefault свойству MyMessage экземпляра TClassProperty в конструкторе класса TClassProperty.

Оставшиеся инструкции в этих обработчиках также похожи. Объект назначается интерфейсной переменной в третьей инструкции, после чего используется интерфейсная ссылка, чтобы назначить текст сообщения и показать это сообщение. Если Вы запустите этот пример, Вы увидите, что нажатие на кнопку, помеченную как Object with class type interface property, покажет выполнение класса TYesDefault, а нажатие на кнопку, помеченную как Object with interface type interface property, покажет выполнение, предусмотренное классом TNodefault.

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

Комментарии к реализации интерфейсов делегированием

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

Проблема может быть видна ясно, если Вы сравните последовательность отображения диалоговых окон, нажимая на кнопки двух реализаций интерфейса на основной форме DEMOINT. Действительно, если Вы нажмете на кнопку Object with class type interface property, Вы увидите последовательность диалоговых окон в таком порядке: диалоговое окно TYesDefault ShowMessage, показывающее, что будет освобожден интерфейс, диалоговое окно деструктора TYesDefault, показывающее, что будет освобождаться объект, и наконец диалоговое окно, показывающее, что объект освободился.

Сравните эту последовательность со следующей, которая происходит, когда Вы нажимаете на кнопку Object with interface type interface property: диалоговое окно TNodefault ShowMessage, сопровождается диалоговым окном, показывающим, что будет освобожден интерфейс, затем диалоговое окно, показывающее, что будет освобожден объект, сопровождается диалоговым окном деструктора TNoDefault, и наконец диалоговое окно, показывающее, что объект освободился.

Различие в том, что освобождаемая интерфейсная переменная вызывает разрушение объекта, назначенного свойству TClassProperty.MyMessage, но не оказывает никакого эффекта на объект, назначенный свойству TInterfacePropery.MyMessage. Объект назначенный свойству TInterfacePropery.MyMessage не освободится, пока не освободится экземпляр TInterfaceProperty. Правильно именно это последнее поведение.

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

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

Интерфейсы и COM

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

COM это двоичный стандарт, означающий, что объекты с которыми Вы работаете могут быть откомпилированы любым компилятором, не только Delphi. Фактически, при использовании COM, объекты с которыми Вы работаете, обычно скомпилированы не Delphi а чем-нибудь другим, например Visual C. Но это хорошо. Пока Вы используете в параметрах стандартные OLE типы, Ваши приложения Delphi смогут связываться с этими объектами, вызывая их методы, читая и записывая их свойства.

Но интерфейсы только один аспект COM. Есть очень много других вопросов, включая такие, как обеспечение поддержки для разнообразных потоковых моделей, поддерживаемых COM серверами. Следовательно, Я больше не буду говорить о COM в этой статье. За дополнительной информацией о COM, OLE автоматизации и ActiveX обращайтесь к другим статьям, прошлым и будущим в Delphi Informant Magazine.

Интерфейсы и Open Tools API Delphi

В начале первой части этой серии статей, Я упомянул, что одной из причин, другого взгляда на интерфейсы, стала дополнительная уверенность в использовании интерфейсов в самом Delphi. Определенно, с каждой новой версией Delphi, ее собственное поведение все больше определяется интерфейсами. Хороший пример этого - Open Tools API. В Delphi 5, больше чем когда-либо, требуется, чтобы Вы, создавая собственные расширения IDE Delphi, создавали классы, которые реализуют определенные интерфейсы. Например, чтобы добавить индивидуальную привязку клавиш в редакторе Delphi, Вы создаете и регистрируете класс, который реализует интерфейс IOTAKeyboardBinding (OTA сокращение от Open Tools API).

Заключение

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

Проект на который ссылается эта статья доступен для загрузки.

Cary Jensen is president of Jensen Data Systems, Inc., a Houston-based database development company. He is co-author of 17 books, including Oracle JDeveloper [Oracle Press, 1998], JBuilder Essentials [Osborne/McGraw-Hill, 1998], and Delphi in Depth [Osborne/McGraw-Hill, 1996]. He is a Contributing Editor of Delphi Informant Magazine, and an internationally respected trainer of Delphi and Java. For more information, visit http://www.jensendatasystems.com, or e-mail Cary at mailto:cjensen@compuserve.com

By Cary Jensen, Ph.D.

Перевел Мотов Олег mailto:olegm@tebuk.parma.ru http://olegmotov.h1.ru