Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

DiVM / OOP / 12_116608_1_51491

.pdf
Скачиваний:
18
Добавлен:
11.05.2015
Размер:
6.45 Mб
Скачать

программ разрабатываются на различных языках программирования, например Delphi, C++, Visual Basic и др., технология COM стандартизирует формат взаимодействия между объектами на уровне двоичного представления в оперативной памяти. Согласно технологии COM взаимодействие между объектами осуществляется посредством так называемых интерфейсов. Рассмотрим, что же они собой представляют и как с ними работают.

6.1. Понятие интерфейса

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

Интерфейс = Объект – Реализация

В отличие от объекта интерфейс сам ничего “не помнит” и ничего “не умеет делать”; он является всего лишь "разъемом" для работы с объектом. Объект может поддерживать много интерфейсов и выступать в разных ролях в зависимости от того, через какой интерфейс вы его используете. Совершенно различные по структуре объекты, поддерживающие один и тот же интерфейс, являются взаимозаменяемыми. Не важно, есть у объектов общий предок или нет. В данном случае интерфейс служит их дополнительным общим предком.

6.2. Описание интерфейса

В языке Delphi интерфейсы описываются в секции type глобального блока. Описание начинается с ключевого слова interface и заканчивается ключевым словом end. По форме объявления интерфейсы похожи на обычные классы, но в отличие от классов:

интерфейсы не могут содержать поля;

интерфейсы не могут содержать конструкторы и деструкторы;

все атрибуты интерфейсов являются общедоступными (public);

все методы интерфейсов являются абстрактными (virtual, abstract).

Приведем пример интерфейса и сразу заметим, что интерфейсам принято давать имена, начинающиеся с буквы I (от англ. Interface):

type

ITextReader = interface // Методы

function NextLine: Boolean; // Свойства

property Active: Boolean; property ItemCount: Integer;

property Items[Index: Integer]: string; property EndOfFile: Boolean;

end;

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

Объяснение состоит в следующем. Не определив интерфейс ITextReader, невозможно разместить класс TTextReader в DLL-библиотеке и обеспечить доступ к нему из EXEпрограммы. Создавая DLL-библиотеку, мы с помощью оператора uses должны включить модуль ReadersUnit в проект библиотеки. Создавая EXE-программу, мы должны включить модуль ReadersUnit и в нее, чтобы воспользоваться описанием класса TTextReader. Но

181

тогда весь программный код класса попадет внутрь EXE-файла, а это именно то, от чего мы хотим избавиться. Решение проблемы обеспечивается введением понятия интерфейса.

Чтобы вам было легче разобраться с интерфейсом ITextReader, мы привели его незаконченный вариант. Компиляция интерфейса в таком виде приведет к ошибкам: для свойств не указаны методы чтения и записи. Полное описание интерфейса выглядит так:

type

ITextReader = interface // Методы

function NextLine: Boolean;

procedure SetActive(const Active: Boolean); function GetActive: Boolean;

function GetItemCount: Integer;

function GetItem(Index: Integer): string; function GetEndOfFile: Boolean;

// Свойства

property Active: Boolean read GetActive write SetActive; property Items[Index: Integer]: string read GetItem; default; property ItemCount: Integer read GetItemCount;

property EndOfFile: Boolean read GetEndOfFile; end;

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

6.3. Расширение интерфейса

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

type

IExtendedTextReader = interface(ITextReader) procedure SkipLines(Count: Integer);

end;

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

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

type

ITextReader = interface

...

end;

эквивалентно следующему:

type

ITextReader = interface(IInterface)

...

end;

Мы рекомендуем использовать вторую, более полную форму записи. Описание интерфейса IInterface находится в стандартном модуле System:

182

type

IInterface = interface ['{00000000-0000-0000-C000-000000000046}']

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall; end;

Непонятная последовательность нулей и других цифр в квадратных скобках — это так называемый глобально-уникальный идентификатор интерфейса. Мы к нему еще вернемся, а сейчас рассмотрим методы.

Методы интерфейса IInterface явно или неявно попадают во все интерфейсы и имеют особое назначение. Метод QueryInterface нужен для того, чтобы, имея некоторый интерфейс, запросить у объекта другой интерфейс. Этот метод автоматически вызывается при преобразовании одних интерфейсов в другие. Метод _AddRef автоматически вызывается при присваивании значения интерфейсной переменной. Метод _Release автоматически вызывается при уничтожении интерфейсной переменной. Последние два метода позволяют организовать подсчет ссылок на объект и автоматическое уничтожение объекта, когда количество ссылок на него становится равным нулю. Вызовы всех трех методов генерируются компилятором автоматически, и вызывать их явно нет необходимости, однако программист должен позаботиться об их реализации.

6.4. Глобально-уникальный идентификатор интерфейса

Интерфейс является особым типом данных: он может быть реализован в одной программе, а использоваться из другой. Для этого нужно обеспечить идентификацию интерфейса при межпрограммном взаимодействии. Понятно, что программный идентификатор интерфейса для этого не подходит — разные программы пишутся разными людьми, а разные люди подчас дают одинаковые имена своим творениям. Поэтому каждому интерфейсу выдается своеобразный «паспорт» — глобально-уникальный идентификатор (Globally Unique Identifier

— GUID).

Глобально-уникальный идентификатор — это 16-ти байтовое число, представленное в виде заключенной в фигурные скобки последовательности шестнадцатеричных цифр:

{DC601962-28E5-4BF7-9583-0CE22B605045}

В среде Delphi глобально-уникальный идентификатор описывается типом данных TGUID:

type

PGUID = ^TGUID;

TGUID = packed record D1: Longword;

D2: Word;

D3: Word;

D4: array[0..7] of Byte; end;

Константы с типом TGUID разрешено инициализировать строковым представлением глобально-уникального идентификатора. Компилятор сам преобразует строку в запись с типом TGUID. Пример:

const

InterfaceID: TGUID = '{DC601962-28E5-4BF7-9583-0CE22B605045}';

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

183

type

IInterface = interface ['{00000000-0000-0000-C000-000000000046}']

...

end;

В будущем нашему интерфейсу ITextReader понадобится глобально-уникальный идентификатор. Но как его выбрать так, чтобы он оказался уникальным? Очень просто — нажмите в редакторе кода комбинацию клавиш Ctrl+Shift+G.

type

ITextReader = interface

['{DC601962-28E5-4BF7-9583-0CE22B605045}'] // Результат нажатия Ctrl+Shift+G

...

end;

Генерация глобально-уникальных идентификаторов осуществляется системой Windows по специальному алгоритму, в котором задействуется адрес сетевого адаптера, текущее время и генератор случайных чисел. Можете смело полагаться на уникальность всех получаемых идентификаторов.

Наличие глобально-уникального идентификатора в описании интерфейса не является обязательным, однако использование интерфейса без такого идентификатора ограничено, например, запрещено использовать оператор as для преобразования одних интерфейсов в другие.

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

const

IID_ITextReader: TGUID = '{DC601962-28E5-4BF7-9583-0CE22B605045}';

function TestInterface(const IID: TGUID): Boolean;

begin

...

TestInterface(ITextReader); // эквивалентно

TestInterface(IID_ITextReader);

...

end;

6.5. Реализация интерфейса

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

type

TTextReader = class(TObject, ITextReader)

...

end;

Такая запись означает, что класс TTextReader унаследован от класса TObject и реализует интерфейс ITextReader (см. рисунок 6.1).

184

Рисунок 6.1. Класс TTextReader унаследован от класса TObject и реализует интерфейс ITextReader. Сплошными линиями отмечено наследование классов, а пунктирной линией — реализация интерфейса классом.

Класс, реализующий интерфейс, должен содержать код для всех методов интерфейса. Класс TTextReader в модуле ReadersUnit (см. главу 3) вроде бы содержит код для всех методов интерфейса ITextReader, и все, что нужно сделать, — это добавить имя интерфейса в заголовок класса. Сделайте это в модуле ReadersUnit:

unit ReadersUnit;

interface

type

ITextReader = interface

...

end;

TTextReader = class(TObject, ITextReader)

...

end;

Если класс содержит только часть методов интерфейса, то недостающие методы придется добавить. Так в интерфейсе ITextReader описан метод GetActive, а в классе TTextReader такого метода нет. Добавьте метод GetActive в класс TTextReader:

type

TTextReader = class(TObject, ITextReader)

...

function GetActive: Boolean;

...

end;

function TTextReader.GetActive: Boolean; begin

Result := FActive; end;

Но это еще не все. Мы совсем забыли о методах QueryInterface, _AddRef и _Release, которые тоже должны быть реализованы. К счастью, вам нет необходимости ломать голову над реализацией этих методов, поскольку разработчики системы Delphi уже позаботились об этом. Стандартная реализация методов интерфейса IInterface находится в классе TInterfacedObject. Мы его рассмотрим ниже, а сейчас просто унаследуем класс TTextReader от класса TInterfacedObject — и он получит готовую реализацию методов

QueryInterface, _AddRef и _Release.

type

TTextReader = class(TInterfacedObject, ITextReader)

...

end;

Теперь реализация интерфейса ITextReader полностью завершена и можно переходить к использованию объектов класса TTextReader через этот интерфейс.

185

6.6. Использование интерфейса

Для доступа к объекту через интерфейс нужна интерфейсная переменная:

var

Intf: ITextReader;

Интерфейсная переменная занимает в оперативной памяти четыре байта, хранит ссылку на интерфейс объекта и автоматически инициализируется значением nil.

Перед использованием интерфейсную переменную инициализируют значением объектной переменной:

var

Obj: TTextReader; // объектная переменная Intf: ITextReader; // интерфейсная переменная

begin

...

Intf := Obj;

...

end;

После инициализации интерфейсную переменную Intf можно использовать для вызова методов объекта Obj:

Intf.Active :=

True; //

->

Obj.SetActive(True);

Intf.NextLine;

//

->

Obj.NextLine;

Через интерфейсную переменную доступны только те методы и свойства объекта, которые есть в интерфейсе:

Intf.Free; // Ошибка! У интерфейса ITextReadaer нет метода Free.

Obj.Free; // Метод Free можно вызвать только так.

6.7. Реализация нескольких интерфейсов

Один класс может содержать реализацию нескольких интерфейсов. Такая возможность позволяет воплотить в классе несколько понятий. Например, класс TTextReader — "считыватель табличных данных" — может выступить еще в одной роли — "считыватель строк". Для этого он должен реализовать интерфейс IStringIterator:

type

IStringIterator = interface function Next: string; function Finished: Boolean;

end;

Интерфейс IStringIterator предназначен для последовательного доступа к списку строк. Метод Next возвращает очередную строку из списка, метод Finished проверяет, достигнут ли конец списка.

Реализуем интерфейс IStringIterator в классе TTextReader таким образом, чтобы последовательно считывались значения из ячеек таблицы. Например, представьте, что в некотором файле дана таблица:

Aaa Bbb Ccc

Ddd Eee Fff

Ggg Hhh Iii

Чтение этой таблицы через интерфейс IStringIterator вернет следующую последовательность строк:

186

Aaa

Bbb

Ccc

Ddd

Eee

Fff

Ggg

Hhh

Iii

Ниже приведен программный код, обеспечивающий поддержку интерфейса IStringIterator в

классе TTextReader:

type

TTextReader = class(TInterfacedObject, ITextReader, IStringIterator) FColumnIndex: Integer;

function Next: string; function Finished: Boolean;

...

end;

...

function TTextReader.Next: string; begin

if FColumnIndex = ItemCount then // Если пройден последний элемент текущей строки,

begin // то переходим к следующей строке таблицы

NextLine; FColumnIndex := 0;

end;

Result := Items[FColumnIndex]; FColumnIndex := FColumnIndex + 1;

end;

function TTextReader.Finished: string; begin

Result := EndOfFile and (FColumnIndex = ItemCount); end;

Теперь объекты класса TTextReader совместимы сразу с тремя типами данных:

TInterfacedObject, ITextReader, IStringIterator.

var

Obj: TTextReader; Reader: ITextReader;

Iterator: IStringIterator; begin

...

Reader := Obj; // Правильно Iterator := Obj; // Правильно

...

end;

В одном случае объект класса TTextReader рассматривается как считыватель табличных данных, а в другом случае — как обычный список строк с последовательным доступом. Например, если есть две процедуры:

procedure LoadTable(Reader: ITextReader); procedure LoadStrings(Iterator: IStringIterator);

то объект класса TTextReader можно передать в обе процедуры:

LoadTable(Obj); //

Obj

воспринимается

как

ITextReader

LoadStrings(Obj); //

Obj

воспринимается

как

IStringIterator

6.8. Реализация интерфейса несколькими классами

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

187

Рассмотрим пример. Представьте, что есть два класса: TTextReader и TIteratableStringList:

type

TTextReader = class(TInterfacedObject, ITextReader, IStringIterator)

...

end;

TIteratableStringList = class(TStringList, IStringIterator)

...

end;

Схематично полученную иерархию классов можно представить так (рисунок 6.2):

Рисунок 6.2. Иерархия классов, реализующих интерфейсы. Сплошными линиями отмечено наследование классов, а пунктирными линиями — реализация интерфейсов классами.

Объекты классов TTextReader и TIteratableStringList несовместимы между собой. Тем не менее, они совместимы с переменными типа IStringIterator. Это значит, что если есть процедура:

procedure LoadStrings(Iterator: IStringIterator);

то вы можете передавать ей объекты обоих упомянутых классов в качестве аргумента:

var

ReaderObj: TTextReader; StringsObj: TIteratableStringList;

begin

...

LoadStrings(ReaderObj); // Все правильно LoadStrings(StringsObj); // Все правильно

...

end;

6.9. Связывание методов интерфейса с методами класса

Метод интерфейса связывается с методом класса по имени. Если имена по каким-то причинам не совпадают, то можно связать методы явно с помощью специальной конструкции языка Delphi.

Например, в классе TTextReader добавлены методы Next и Finished для поддержки интерфейса IStringIterator. Согласитесь, что существование в одном классе методов Next и NextLine вносит путаницу. По названию метода Next не понятно, что для этого метода является следующим элементом. Поэтому уточним название метода в классе TTextReader и воспользуемся явным связыванием методов, чтобы сохранить имя Next в интерфейсе

IStringIterator:

188

type

TTextReader = class(TInterfacedObject, ITextReader, IStringIterator)

...

function NextItem: string;

function IStringIterator.Next := NextItem; // Явное связывание end;

При работе с объектами класса TTextReader через интерфейс IStringIterator вызов метода Next приводит к вызову метода NextItem:

var

Obj: TTextReader; Intf: IStringIterator;

begin

...

Intf := Obj;

Intf.Next; // -> Obj.NextItem;

...

end;

Очевидно, что связываемые методы должны совпадать по сигнатуре (списку параметров и типу возвращаемого значения).

6.10. Реализация интерфейса вложенным объектом

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

type

TTextParser = class(TInterfacedObject, ITextReader)

...

FTextReader: ITextReader;

property TextReader: ITextReader read FTextReader implements ITextReader;

...

end;

Вэтом примере интерфейс ITextReader в классе TTextParser реализуется не самим классом,

аего внутренней переменной FTextReader.

Очевидно, что внутренний объект должен быть совместим с реализуемым интерфейсом.

6.11. Совместимость интерфейсов

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

type

IExtendedTextReader = interface(ITextReader)

...

end;

то интерфейсной переменной базового типа может быть присвоено значение интерфейсной переменной производного типа:

var

Reader: ITextReader; ExtReader: IExtendedTextReader;

begin

...

Reader := ExtReader; // Правильно

...

end;

Но не наоборот:

189

ExtReader := Reader; // Ошибка!

Правило совместимости интерфейсов чаще всего применяется при передаче параметров в процедуры и функции. Например, если процедура работает с переменными типа

ITextReader,

procedure LoadFrom(const R: ITextReader);

то ей можно передать переменную типа IExtendedTextReader:

LoadFrom(ExtReader);

Заметим, что любая интерфейсная переменная совместима с типом данных IInterface — прародителем всех интерфейсов.

6.12. Совместимость класса и интерфейса

Интерфейсной переменной можно присвоить значение объектной переменной при условии, что объект (точнее его класс) реализует упомянутый интерфейс:

var

Intf: ITextReader; // интерфейсная переменная Obj: TTextReader; // объектная переменная

begin

...

Intf := Obj; // В переменную Intf копируется ссылка на объект Obj

...

end;

Такая совместимость сохраняется в производных классах. Если класс реализует некоторый интерфейс, то и все его производные классы совместимы с этим интерфейсом (см. рисунок

6.3):

type

TTextReader = class(TInterfacedObject, ITextReader)

...

end;

TDelimitedReader = class(TTextReader)

...

end;

var

// интерфейсная переменная

Intf: ITextReader;

Obj: TDelimitedReader;

// объектная переменная

begin

 

...

 

Intf := Obj;

 

...

 

end;

 

Рисунок 6.3. Классы TTextReader, TDelimitedReader и TFixedReader совместимы с интерфейсом ITextReader

Однако, если класс реализует производный интерфейс, то это совсем не означает, что он совместим с базовым интерфейсом (см. рисунок 6.4):

190