Програмування на мові Delphi. Глава 6. Інтерфейси, Різне, Програмування, статті

попередня стаття серії

При програмуванні нерідко виникає необхідність виконати звернення до об’єкта, що знаходиться в іншому завантажувальному модулі, наприклад EXE або DLL. Для вирішення поставленого завдання компанія Microsoft розробила технологію COM (Component Object Model) – компонентну модель об’єктів. Технологія отримала таку назву завдяки тому, що забезпечує створення програмних компонентів – незалежно розроблюваних і поставляються двійкових модулів. Оскільки об’єкти різних програм розробляються на різних мовах програмування, наприклад Delphi, C + +, Visual Basic та ін, технологія COM стандартизує формат взаємодії між об’єктами на рівні двійкового подання до оперативної пам’яті. Згідно технології COM взаємодія між об’єктами здійснюється за допомогою так званих інтерфейсів. Розглянемо, що ж вони собою представляють і як з ними працюють.


6.1. Поняття інтерфейсу


З попередніх глав ви вже знаєте, що собою представляє об’єкт. Уявіть, що вийде, якщо з об’єкта прибрати поля і код всіх методів. Залишиться лише інтерфейс – заголовки методів і опису властивостей. Схематично поняття інтерфейсу можна представити у вигляді формули:


Інтерфейс = Об’єкт – Реалізація


На відміну від об’єкта інтерфейс сам нічого “не пам’ятає” і нічого “не вміє робити”, він є всього лише “роз’ємом” для роботи з об’єктом. Об’єкт може підтримувати багато інтерфейсів і виступати в різних ролях в залежності від того, через який інтерфейс ви його використовуєте. Абсолютно різні за структурою об’єкти, що підтримують один і той же інтерфейс, є взаємозамінними. Не важливо, є у об’єктів загальний предок чи ні. В даному випадку інтерфейс є їх додатковим спільним предком.


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


У мові Delphi інтерфейси описуються в секції type глобального блоку. Опис починається з ключового слова interface і закінчується ключовим словом end. За формою оголошення інтерфейси схожі на звичайні класи, але на відміну від класів:



Наведемо приклад інтерфейсу і відразу зазначимо, що інтерфейсам прийнято давати імена, що починаються з літери 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. Але тоді весь програмний код класу потрапить всередину 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:





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 і полягає в квадратні дужки, наприклад:





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).



Рисунок 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 через цей інтерфейс.


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 поверне наступну послідовність рядків:





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. Реалізація інтерфейсу декількома класами


Кілька абсолютно різних класів можуть містити реалізацію одного і того ж інтерфейсу. З об’єктами таких класів можна працювати так, ніби у них є спільний базовий клас. Інтерфейс виступає аналогом загального базового класу.


Розглянемо приклад. Уявіть, що є два класи: 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:





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;

Але не навпаки:





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):





type
ITextReader = interface(IInterface)

end;
IExtendedTextReader = interface(ITextReader)

end;
TExtendedTextReader = class(TInterfacedObject, IExtendedTextReader)

end;
var
Obj: TExtendedTextReader;
Intf: ITextReader;
begin
… Intf: = Obj; / / Помилка! Клас TExtendedTextReader не реалізує / / Інтерфейс ITextReader.

end;


Рисунок 6.6. Схема отримання програми та DLL-бібліотеки


Але постійте! А як у програмі створювати об’єкти класів, що знаходяться в DLL-бібліотеці? Адже в інтерфейсі немає методів для створення об’єктів! Для цього визначимо в DLL-бібліотеці спеціальну функцію і експортуємо її:





library ReadersLib;

function GetDelimitedReader(const FileName: string;
const Delimiter: Char = “;”): ITextReader;
begin
Result := TDelimitedReader.Create(FileName, Delimiter);
end;
exports
GetDelimitedReader;
begin
end.

У головній програмі імпортуйте функцію GetDelimitedReader, Щоб з її допомогою створювати об’єкти класу TDelimitedReader:





program Example;
uses
ReadersIntf;
function GetDelimitedReader(const FileName: string;
const Delimiter: Char = “;”): ITextReader;
external “ReadersLib.dll” name “GetDelimitedReader”;
var
Intf: ITextReader;
begin
Intf := GetDelimitedReader;

end.

Тепер ви знаєте, як розмістити об’єкти в DLL-бібліотеці. Сміливо користуйтеся динамічно завантажуваними бібліотеками, не втрачаючи переваг ООП.


6.17. Підсумки


Ви прочитали і засвоїли весь матеріал всіх попередніх глав? Тоді поспішаємо вас привітати! Можете сміливо стверджувати, що знаєте мову програмування Delphi. Що ж далі? Вас чекає нова висота – середа програмування Delphi. Зараз ви маєте лише поверхове уявлення про її можливості. Настав час підготувати себе до професійної роботи в середовищі Delphi.

наступна стаття серії

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*