Дмитрий Полщанов дата публикации 09-10-2001 15:05 Еще раз о Plugin's…
Проблема декомпозиции приложения рано или поздно возникает в любом серьезном проекте. Цели декомпозиции могут быть различны, но можно выделить наиболее часто встречающиеся:
- Облегчение сопровождения. Приложение разбивается на функциональные модули так, чтобы можно было без опаски заменить "ошибочный" модуль на (якобы) "исправленный" J.
- Обеспечение "наращиваемости". То есть, путем добавления нового модуля и (может быть) прописывания некоторой информации в реестре Windows (или ini-файле - кому как нравится) основное приложение без перекомпиляции получало бы новые функциональные (или интерфейсные) возможности.
- Обеспечение "взаимозаменяемости" модулей. То есть, спроектировать систему так, чтобы можно было заменить один модуль на другой (поддерживающий, естественно, тот же или расширенный функционал) без потери работоспособности системы в целом.
Наиболее "древним" решением данной задачи является инкапсуляция функционала в dll. К недостаткам данного подхода можно отнести:
- Приходится утомительно долго описывать "экспортные" функции в dll и "рисовать" модули импорта;
- "Взаимозаменяемость" обеспечить в принципе можно, но "наращиваемость" и сопровождение оставляют желать лучшего;
- Наличие "некоторых тонкостей" (типа упаковки дочерних окон в dll - эта тема достаточно широко обсуждалась у Круглого Стола (1) ) вообще затрудняют использование данного метода, тем более для новичка;
Вторым способом (вполне неплохим) является технология COM (от Microsoft (2) ). Это примерно то же самое, что и обычные dll, но добавляются еще и
- Более легкое сопровождение.
- Исключительно легкая взаимозаменяемость.
- Наращиваемость - только и мечтать.
Но без ложки дегтя все равно не обходится. Применительно к Delphi это:
- Дикое разрастание объема выполняемого кода (если, конечно, не использовать компиляцию с пакетами) - но это чревато ослабленной устойчивостью L применительно к dll. С чем это связано, мне определить не удалось - ошибки возникают при выгрузке приложения, но в каком именно месте - осталось невыясненным.
- Обычно такие "навороты" мало кому нужны. Разве что при обеспечении межпроцессного (3) или межкомпьютерного (6) взаимодействия. Кстати, для последнего лучше подходит CORBA…
- Как ни старалась Borland облегчить работу с COM, на мой взгляд то, что получилось абсолютно не удовлетворительно. Мало того, что существуют "некоторые" существенные ограничения на типы входных и выходных параметров, но и методика работы с COM в Delphi оставляет желать лучшего…
Третий способ - использование пакетов. Это как-то обсуждалось в Королевстве (Трофимов Игорь, Подгружаемые модули (plugins) в Delphi). Достоинства данного подхода в том, что суммарный объем выполняемого кода получается меньше, чем в обоих предыдущих способах (особенно, если выключить из рассмотрения "библиотечные" пакеты типа vcl50). По крайней мере, неплохо. Однако есть и недостатки и у данного подхода.
Это:
- Практически невозможность дальнейшего сопровождения. При любом изменении виртуальной таблицы методов (VMT) (5) базового класса - пакеты, их использующие, становятся неработоспособными. И даже более того - опасными!
- Условная взаимозаменяемость. Так или иначе, придется скрупулезно проверять вызов каждой функции и каждого метода (6).
- "Усложненность" разработки. Как показывает практика, при уровне "наследуемости" пакетов больше трех (7) процесс компиляции понравится только мазохистам (а если учесть 1 пункт, то проще на все плюнуть J)
Если учесть, что пакеты Delphi - то же самое, что и обычные dll (8), а COM (в большинстве случаев) так же инкапсулируется в dll, то напрашивается желание совместить достоинства и тех, и других. Что я сейчас и попытаюсь сделать. Сразу хочу оговориться, что данная статья рассчитана как на новичков, так и на "продолжающих". Это значит, что иногда я буду углубляться в "излишнее разжевывание", но при этом рассчитывать на некоторый "базис" первоначальных знаний. Но даже без последних не трудно будет использовать предлагаемую методику. Даже более того - надеюсь, она поможет в освоении COM…
Я здесь не ставлю своей задачей открыть что-либо новое в программировании вообще и на Delphi в частности. Я только хочу показать
во-первых, как можно эффективно использовать встроенные в компилятор средства поддержки COM (при этом не таская за собой ее громоздкую библиотеку поддержки);
и во-вторых, предложить небольшую модернизацию метода Игоря Трофимова;
Предлагаемая здесь методика опробирована на рабочем проекте с достаточно приемлимым результатом.
Давайте сделаем базовый проект, обеспечивающий динамическую подгрузку пакета. На самом деле это достаточно тривиально, но нам необходимо
- начать с чего-нибудь привычного (а кому это не привычно - он прочувствует, что это не так сложно, как это кажется на первый взгляд)
- просто иметь некоторую стартовую точку
- показать, что и разработанные на текущий момент проекты могут быть легко модернизированы с учетом данной методики (возможно, их придется переписывать заново (или начисто, в зависимости от того, как к этому относиться!), - но это даже иногда полезно ).
Для начала спроектируем первое приближение главного приложения. Я хочу показать использование как диалоговых, так и дочерних окон, поэтому главное окно приложения сделаем MDIFrom с созданием всех сопутствующих MDI атрибутов (типа меню Window). Помимо прочего, делаем меню Help (дань привычки J делать приложения со справкой). В качестве основы для обработки команд меню будем использовать TActionList (9).
Завершив эти "магические пассы", добавляем следующее: в секцию private вносим переменную FPackageHandle типа THandle. Она будет хранить дескриптор пакета. Туда же добавляем процедуру LoadPluginPackage, которая будет непосредственно выполнять загрузку пакета plugin.bpl.
Вот текст этой процедуры
procedure TForm1.LoadPluginPackage;
var
FileName: TFileName;
Begin
// предполагаем, что пакет хранится в том же каталоге, что и исполняемое приложение
FileName := ExtractFilePath(Application.ExeName);
FileName := FileName + 'plugin.bpl';
// Загружаем пакет
FPackageHandle := LoadPackage(FileName);
if FPackageHandle = 0 then RaiseLastWin32Error() // пакет не загружен, выбрасываем исключение
else MessageBox(Handle, 'Пакет plugin загружен',
'Информация', MB_APPLMODAL+MB_ICONINFORMATION+MB_OK);
end;
Теперь сделаем собственно пакет (10) . В него поместим две формы, одну из которых сделаем дочерней (MDIChild), а на другую положим две кнопки (Ok и Cancel).
Далее организуем в главной форме загрузку пакета и вызов из него форм. Для этого на OnShow делаем вызов LoadPluginPackage и добавляем actions в ActionList:
Для дочерней формы
procedure TForm1.aOpenExecute(Sender: TObject);
var
frmClass: TFormClass;
frm: TForm;
begin
frmClass := TFormClass(GetClass('TfrmChild')); // получаем класс дочернего окна
if not Assigned(frmClass) then
begin
MessageBox(Handle, PChar(Format('Не найден класс %s', ['TfrmDialogFrom'])),
'Ошибка',
MB_APPLMODAL+MB_ICONERROR+MB_OK);
Exit;
end;
frm := frmClass.Create(Self); // создаем дочернее окно
end;
Для диалога
procedure TForm1.aOpenDialogExecute(Sender: TObject);
var
frmClass: TFormClass;
begin
frmClass := TFormClass(GetClass('TfrmDialogFrom')); // получаем класс диалогового окна
if not Assigned(frmClass) then
begin
MessageBox(Handle, PChar(Format('Не найден класс %s', ['TfrmDialogFrom'])),
'Ошибка',
MB_APPLMODAL+MB_ICONERROR+MB_OK);
Exit;
end;
// создаем и показываем окно диалога
with frmClass.Create(Self) do
try
case ShowModal of
mrOk: MessageDlg('Выбрано Ok!', mtInformation, [mbOk], 0);
mrCancel: MessageDlg('Выбрано Cancel!', mtInformation, [mbOk], 0);
else MessageDlg('Выбрано хрен знает что!', mtInformation, [mbOk], 0);
end;
finally
Free();
end;
end;
Плюс ко всему добавляем обработчик OnUpdate на все action'ы для обеспеченя корректного вызова
procedure TForm1.aOpenUpdate(Sender: TObject);
begin
aOpen.Enabled := FPackageHandle > 0;
aOpenDialog.Enabled := FPackageHandle > 0;
end;
Полный исходный код находится в архиве (каталог Step1)
Часть 2. Доступ к объектам пакета. |
Попытаемся наладить связь между главной формой и пакетом. То есть, мы ставим себе задачу вызова некоторой (или некоторых) функции/процедур формы из пакета при условии того, что мы не знаем действительный тип этой формы и всего набора поддерживаемых ею функций и процедур. Для осуществления этого воспользуемся технологией COM. Точнее той ее части, которая поддерживается Delphi на уровне языка.
Для того, чтобы новичкам было все ясно, следует немного углубиться в понятие интерфейса. Я полагаю, что вы знакомы с понятием виртуальной таблицы методов (VMT). Именно она является источником и тремя составными частями ООП (11). Для поддержки COM в Delphi был введен новый, особый тип interface, который позволяет "поименовать" куски виртуальной таблицы методов. Способ этого именования достаточно уникален - 16-байтовое число (12), которое присваивается каждому такому куску. Есть мнение, что оно статистически уникально (13). Синтаксис данного типа следующий
type
IMyInitialize = interface
['{7D501741-B419-11D5-915B-ED714AED3037}'] // то самое 16-битное число в строковом
// представлении. Получается
// в Delphi нажатием клавиш Ctrl+g.
procedure InitializeForm(const ACaption: String); // а это процедура интерфейса.
end;
Помимо прочего компоненты Delphi содержат метод, позволяющий получать этот кусок виртуальной таблицы. Причем этих методов два.
- Первый имеет название QueryInterface. Для любителей COM он является привычным, так как используется в оном вдоль и поперек.
- Второй называется GetInterface.
Разница этими методами в том, что у QueryInterface нужно проверять результат на S_OK, а у GetInterface на Boolean (14) .
Теперь давайте прикрутим к нашей системе интерфейсы и покажем, как с ними работать. Для этого создаем новый модуль (он достаточно короткий, и я здесь привожу его полностью)
unit CommonInterfaces;
interface
type
IMyInitialize = interface
['{7D501741-B419-11D5-915B-ED714AED3037}']
procedure InitializeForm(const ACaption: String);
end;
IMyHello = interface
['{7D501742-B419-11D5-915B-ED714AED3037}']
function ShowHello(AText: String): String;
end;
implementation
end.
Далее, внесем изменения в наш пакет, учитывающий поддержку интерфейсов
- 1. для дочерней формы:
-
type
TfrmChild = class(TForm, IMyInitialize, IMyHello) // так наследуются интерфейсы
Private
{ описание методов IMyInitialize }
procedure InitializeForm(const ACaption: String);
{ описание методов IMyHello }
function ShowHello(AText: String): String;
end;
Что мы тут сделали? Мы сказали, что TfrmChild является наследником TForm, но помимо методов TForm VMT класа TfrmChild содержит еще два цельных куска, один из которых идентичен VMT IMyInitialize, а второй VMT IMyHello.
- 2. для диалоговой формы:
-
type
TfrmDialogFrom = class(TForm, IMyInitialize)
BitBtn1: TBitBtn;
BitBtn2: TBitBtn;
Label1: TLabel;
private
{ описание методов IMyInitialize }
procedure InitializeForm(const ACaption: String);
end;
Реализация этих методов проста, ее можно смотреть в архиве (Step2).
Соответственно (для вызова этих методов), немного корректируем главную форму…
procedure TForm1.aOpenExecute(Sender: TObject);
var
frmClass: TFormClass;
frm: TForm;
MyInitialize: IMyInitialize;
begin
frmClass := TFormClass(GetClass('TfrmChild'));
if not Assigned(frmClass) then
begin
MessageBox(Handle, PChar(Format(' Не найден класс %s', ['TfrmDialogFrom'])),
'Ошибка',
MB_APPLMODAL+MB_ICONERROR+MB_OK);
exit;
end;
frm := frmClass.Create(Self);
// производим вызов метода интерфейса
if frm.GetInterface(IMyInitialize, MyInitialize) then
begin // интерфейс поддерживается формой, можно вызывать его методы
MyInitialize.InitializeForm(Format('Дочернее окно ? %d', [Tag]));
Tag := Tag + 1;
end
else raise Exception.CreateFmt('Интерфейс %s не поддерживается классом %s',
['ImyInitialize', frm.GetClassName]);
end;
procedure TForm1.aOpenDialogExecute(Sender: TObject);
var
frmClass: TFormClass;
MyInitialize: IMyInitialize;
begin
frmClass := TFormClass(GetClass('TfrmDialogFrom'));
if not Assigned(frmClass) then
begin
MessageBox(Handle, PChar(Format('Не найден класс %s', ['TfrmDialogFrom'])),
'Внимание!',
MB_APPLMODAL+MB_ICONERROR+MB_OK);
Exit;
end;
with frmClass.Create(Self) do
try
if GetInterface(IMyInitialize, MyInitialize) then
begin // Интерфейс поддерживается фомой, вызываем его метод
MyInitialize.InitializeForm(' Диалог');
end
else raise Exception.CreateFmt('Интерфейс %s не поддерживается классом %s',
['ImyInitialize', frm.GetClassName]);
case ShowModal of
mrOk: MessageDlg('Ok!', mtInformation, [mbOk], 0);
mrCancel: MessageDlg('Cancel!', mtInformation, [mbOk], 0);
else MessageDlg('Неизвестная распальцовка!', mtInformation, [mbOk], 0);
end;
finally
Free();
end;
end;
Полностью весь проект смотрите в архиве (Step2.zip).
Теперь что мы имеем.
- Во-первых, мы не знаем действительный тип как дочернего, так и диалогового окон. Но между тем вызываем функции, входящие в его VMT.
- Во-вторых, мы запросто можем поменять наш пакет на другой. Имена классов форм пакета особого значения не имеют - их можно сохранять в файле настроек или реестре и подгружать при инициализации основного приложения. Единственное, что необходимо неукоснительно соблюдать - дочернее и диалоговое окно ДОЛЖНЫ поддерживать необходимые интерфейсы.
- В третьих, мы можем КАК УГОДНО изменять формы пакета (включая изменения самой виртуальной таблицы методов, естественно, не затрагивая описания интерфейсов) - общая система приложение-пакет останутся в рабочем состоянии.
Часть 3. Взаимодействие пакета с приложением |
При разработке проекта довольно часто встречается ситуация, когда одна из форм (модулей данных, компонент или, наконец, просто объектов) обращается к методам второй формы, а та, в свою очередь, нуждается в вызове методов (или в доступе к свойствам) первой. Иногда эта ситуация вообще трудно разрешима (если оказываются необходимыми перекрестные ссылки в интерфейсных частях модулей — это недопустимо правилами языка)(15) . Очень часто взаимодействие модулей проекта, форм и т.д. оказывается до такой степени перепутанным, что разобраться в этих хитросплетениях бывает тяжело (особенно если этот проект передается для дальнейшего сопровождения и доработки другому программисту). Часть таких проблем вполне может снять использование интерфейсов. Действительно, в предыдущей части для использования методов форм из пакета нам не потребовалось подключать модули, содержащие их реализацию. Что помешает использовать ту же технологию и в обратном направлении?
Попробуем это реализовать. В модуль CommonInterfaces добавляем новый интерфейс
ICallBackInterface = interface
['{7D501743-B419-11D5-915B-ED714AED3037}']
procedure Callback(Text: String);
end;
Добавляем этот интерфейс к главной форме приложения
type
TForm1 = class(TForm, ICallBackInterface)
MainMenu: TMainMenu;
…
protected
{ ICallBackInterface }
procedure Callback(Text: String);
…
end;
var
Form1: TForm1;
implementation
…
procedure TForm1.Callback(Text: String);
begin
ShowMessage('Из главной формы с приветом "' + Text + '"');
end;
…
А теперь возвращаемся в пакет и пробуем вызвать метод Callback главной формы из пакета. В дочерней форме TfrmChild создаем TAction aQueryInMainForm, цепляем его в меню и создаем реализацию OnExecute
procedure TfrmChild.aQueryInMainFormExecute(Sender: TObject);
var
CallBackInterface: ICallBackInterface;
begin
if Application.MainForm.GetInterface(ICallBackInterface, CallBackInterface) then
CallBackInterface.Callback('Привет от дочерней формы ' + Caption);
end;
Теперь запускаем и проверяем, что все у нас работает как надо.
Можно несколько усложнить наш пример и наглядно продемонстрировать новые преимущества данной методики. Давайте добавим на форму объект TListBox и изменим реализацию метода ShowHello из интерфейса IMyHello следующим образом
function TfrmChild.ShowHello(AText: String): String;
begin
InputQuery('Вот что спросили', AText, Result);
ListBox1.Items.Add('Вот что спросили:');
ListBox1.Items.Add(AText);
ListBox1.Items.Add('Вот что ответили:');
ListBox1.Items.Add(Result);
ListBox1.Items.Add('');
end;
Идем к форме TfrmDialogFrom, добавляем туда большую кнопку, на OnClick которой пишем следующее:
procedure TfrmDialogFrom.Button1Click(Sender: TObject);
var
MyHello: IMyHello;
Result: String;
begin
if Assigned(Application.MainForm.ActiveMDIChild) and
Application.MainForm.ActiveMDIChild.GetInterface(IMyHello, MyHello) then
begin
Result := MyHello.ShowHello('Где начало того конца, которым начинается начало?');
ShowMessage(Result);
end;
end;
Если задуматься над этой процедурой, то станет ясно, что нам не важен тип активной дочерней формы и местоположение реализации этого типа (в главном приложении находится ее модуль, в том же пакете, что и TfrmDialogFrom, или где-нибудь еще). Мы просто обнаружили, что есть какая-то активная форма, спросили ее на предмет поддержки конкретного интерфейса и вызвали его метод.
Полный исходный код этой части находится в архиве (каталог Step3)
Часть 4. Некоторые нюансы |
В связи с тем, что мы "разбиваем" виртуальную таблицу методов наших форм на "куски"-интерфейсы, возможны ситуации, когда несколько интерфейсов будут содержать методы с одинаковым названием. Способ обработки таких случаев известен программистам, работавшим с COM. Для тех, кому он неизвестен, я сейчас его продемонстрирую.
Добавим еще один интерфейс в модуль CommonInterfaces и назавем его ICallbackInterface2. В интерфейсе опишем процедуру с названием, пересекающимся с ICallbackInterface:
ICallbackInterface2 = interface
['{7D501744-B419-11D5-915B-ED714AED3037}']
procedure Callback(Text: String);
end;
Теперь введем этот интерфейс в главную форму:
type
TForm1 = class(TForm, ICallBackInterface, ICallbackInterface2)
MainMenu: TMainMenu;
File1: TMenuItem;
…
Чтобы компилятор правильно различал вызовы методов Callback от разных интерфейсов, секцию protected перепишем следующим образом:
…
protected
// перенаправляем вызов через ICallBackInterface к процедуре Callback1
procedure ICallBackInterface.Callback = Callback1;
// перенаправляем вызов через ICallBackInterface2 к процедуре Callback2
procedure ICallBackInterface2.Callback = Callback2;
procedure Callback1(Text: String);
procedure Callback2(Text: String);
…
end;
…
procedure TForm1.Callback1(Text: String);
begin
ShowMessage('Из главной формы 1 "' + Text + '"');
end;
procedure TForm1.Callback2(Text: String);
begin
ShowMessage(' Из главной формы 2 "' + Text + '"');
end;
И, наконец, в форме TfrmChild нашего пакета строим вызовы этих методов
type
TfrmChild = class(TForm, IMyInitialize, IMyHello)
…
aQueryInMainForm: TAction;
aQueryInMainForm2: TAction;
…
procedure aQueryInMainFormExecute(Sender: TObject);
procedure aQueryInMainForm2Execute(Sender: TObject);
private
…
procedure TfrmChild.aQueryInMainFormExecute(Sender: TObject);
var
CallBackInterface: ICallBackInterface;
begin
if Application.MainForm.GetInterface(ICallBackInterface, CallBackInterface) then
CallBackInterface.Callback('Привет от ' + Caption);
end;
procedure TfrmChild.aQueryInMainForm2Execute(Sender: TObject);
var
CallBackInterface: ICallBackInterface2;
begin
if Application.MainForm.GetInterface(ICallBackInterface2, CallBackInterface) then
CallBackInterface.Callback('Привет от ' + Caption);
end;
…
Запускаем и убеждаемся в том, что вызываются действительно нужные методы.
Полный исходный код этой части находится в архиве (каталог Step4).
Еще один нюанс, известный программистам COM. Ничто не запрещает вводить в интерфейсы обычные свойства Deplhi (для более простого моделирования). Ограничением, естественно, является только то, что интерфейс не может содержать полей-данных. В интерфейсы должны быть описаны только методы. Вот пример интерфейса содержащего свойства
IMainForm = interface
['{765B2E71-B81C-11D5-9160-C43E6EC62937}']
function GetCaption: TCaption;
procedure SetCaption(const Value: TCaption);
function GetFont: TFont;
procedure SetFont(conts Value: TFont);
function GetSelf: TForm;
property Caption: TCaption read GetCaption write SetCaption;
property Font: TFont read GetFont write SetFont;
property Self: TForm read GetSelf;
end
Естественно, при наследовании некоторым классом (скажем, какой-нибудь формой) этого интерфейса необходимо в него ввести реализацию методов GetCaption, SetCaption, GetFont, SetFont, GetSelf .
Ну и напоследок, интерфейсы можно наследовать так же, как и обычные классы. При чем это наследование может быть множественным (как в С++). Пример:
У нас был интерфейс ICallBackInterface:
ICallBackInterface = interface
['{7D501743-B419-11D5-915B-ED714AED3037}']
procedure Callback(Text: String);
end;
Добавляем еще один интерфейс, расширяющий поведение ICallBackInterface
ICallBackInterfaceEx = interface(ICallBackInterface)
['{7D501743-B419-11D5-915B-ED714AED3038}']
procedure CallbackEx(Text: String);
end;
, а в главной форме поменяем наследование
TForm1 = class(TForm, ICallBackInterface, ICallbackInterfaceEx)
Теперь после компиляции что мы получим?
Вызов Application.MainForm.GetInterface(ICallBackInterface, CallBackInterface) всегда будет возвращать ссылку на кусок виртуальной таблицы методов, содержащей процедуру Callback и только ее (то есть, действительно ссылку ICallBackInterface, хотя мы его явно не наследовали).
А вот вызов Application.MainForm.GetInterface(ICallBackInterfaceEx, CallBackInterface) будет возвращать ссылку на кусок виртуальной таблицы методов, содержащей как процедуру Callback, так и CallbackEx.
Отсюда можно сделать следующие выводы:
- Старые приложения (или пакеты - все зависит от места использования интерфейса), не знающего ICallBackInterfaceEx, будут вызывать ICallBackInterface и останутся в работоспособном состоянии.
- Новые приложения (или пакеты), уже имеющие сведения о ICallBackInterfaceEx, вполне могут вызывать как ICallBackInterfaceEx, так и ICallBackInterface (в зависимости от прихоти программиста).
То есть, значительно облегчается сопровождения декомпозированного приложения (что так знакомо программистам COM).
До сих пор для получения интерфейса объекта я использовал функцию GetInterface. Она прекрасно работает и удобна в использовании, но имеет существенные ограничения. Прежде всего, эта функция не виртуальная. То есть, вы не сможете переопределить ее поведение в классах-наследниках. А делает эта функция только одно - сканирует локальную VMT объекта на предмет получения требуемого куска VMT. Однако, начиная от TComponent, компоненты Delphi содержат функцию, делающую почти то же самое, но являющуюся виртуальной. Под "почти" я имею ввиду то, что эта функция вызывает GetInterface, но осуществляет еще дополнительные проверки и имеет немного другой формат вызова. Эта функция в последствии (17) принимает участие в COM программировании и имеет наименование QueryInterface (18) .
Функция определяется так:
function QueryInterface(const IID: TGUID; out Obj): HResult; virtual; stdcall;
Функция возвращает HResult (целое число, содержащее код ошибки) для определения успешности или не успешности ее выполнения. Для преобразования этого значения в boolean (если нет необходимости анализировать непосредственно код ошибки и вас интересует лишь фактическое "да" или "нет") имеется дополнительная функция Succeeded. Любой вызов GetInterface из приведенных выше примеров можно заменить на примерно следующий:
if Succeeded(QueryInterface(IMyHello, MyHello)) then …
Однако есть одна маленькая неприятность - функция QueryInterface описана в секции protected класса TComponent. Это означает, что вы не можете ее вызвать нигде, кроме как внутри методов данного класса TComponent. То есть, строка Application.MainForm.QueryInterface(…) не будет компилироваться. Из этого есть два выхода. Первый заключается в получении ЛЮБОГО (19) интерфейса объекта через вызов GetInterface (20) и через него вызывать функцию QueryInterface. Для этих целей можно написать обобщенную процедуру, скажем так
function QueryInterface(const AObject: TObject, const IID: TGUID; out Obj): HResult;
begin
if AObject.GetInterface(IID, Obj) then Result := S_OK
else Result := E_NOINTERFACE;
end;
Второй заключается в написании наследников всех (или почти всех) используемых базовых компонент, в которых эта функция перемещается в секцию public. Примерно вот так:
type
TInterfacedForm = class(TForm, IUnknown)
public
function QueryInterface(const IID: TGUID; out Obj): HResult; override;
end;
…
implementation
function TInterfacedForm .QueryInterface(const IID: TGUID; out Obj): HResult;
begin
Result := inherited QueryInterface(IID, Obj);
end;
end.
Если при этом все необходимые компоненты проекта (в частности, главную форму приложения) наследовать от них, тогда в любой точке проекта можно будет построить следующий вызов.
var
MainForm: TInterfacedForm;
begin
if Application.MainForm is TInterfacedForm then
begin
MainForm := Application.MainForm is TInterfacedForm;
if Succeeded(MainForm.QueryInterface(IMyHello, MyHello)) then …
…
end;
end;
Остается заметить, что этот модуль желательно оформить в виде отдельного пакета, установить его в системе и компилировать с ним как основное приложение, так и пакеты-плугины.
Теперь, после всего выше сказанного, нетрудно осуществить непосредственную агрегацию. Первое место, где она с успехом может быть применена - это приложения с использованием БД. Обычно в этом случае основное приложение имеет (помимо главной формы) один или несколько модулей данных (наследников TDataModule), содержащих коннект к БД и бизнес логику приложения. Чтобы явно подчеркнуть непосредственно агрегацию, сделаем главную форму не наследующей никакого интерфейса. Между тем, оказывается возможным (с помощью простой, но довольно обобщенной махинации) запрашивать требуемые дочерней форме интерфейсы и выполнять над ними работу. Исходный текст проекта см. в архиве (каталог Step5). Код проекта мал, упрощен насколько это возможно (21) и вряд ли нуждается в особых комментариях.
Описанная здесь методика не является панацеей от плохого программирования и других сложностей, которые сам себе создает программист. Но в ряде случаев она может позволить построит более "прозрачную" систему и облегчить ее сопровождение. При "правильном" проектировании в последствии будет легче или перевести всю систему на COM (22) , или довесить основное приложение OLE автоматизацией (23). Во всяком случае, данный способ позволит относительно безопасно потренироваться на "рабочем проекте", поизучать интерфейсы и работу с ними и т.д. Способы ее использования ограничены лишь вашей фантазией программиста.
У данной методики, несомненно, есть и недостатки. Точнее особенности, на которые следует обратить внимание.
- Наследование интерфейсов есть наследование интерфейсов, но не реализации. Реализацию каждый раз придется писать заново. Это в худшем случае. Но ничто не мешает создать "базовый" набор классов, содержащий реализации основных интерфейсов, оформить их в пакет и использовать в дальнейшем (дописывая лишь индивидуальные особенности) (24) .
- Освобождать интерфейсы напрямую нельзя. Delphi это делает автоматически, вызывая неявно функцию _Release. Точно так же, при инициализации интерфейса неявно вызывается функция _AddRef. Эти функции оперируют так называемым "счетчиком ссылок" - целой переменной. хранящейся в объекте. _AddRef его увеличивает, а _Release уменьшает. Когда счетчик ссылок станет равным нулю, функция _Release может вызвать метод Free объекта, содержащего интерфейс. А последнее обстоятельство чревато внезапным исключением, приводящим к катастрофе всего приложения. Следует проследить за этим обстоятельством. Одним из способов его обхода является явный вызов _AddRef в конструкторе объекта - это гарантировано увеличит счетчик на 1 и позволит объекту оставаться в памяти до явного вызова деструктора. Однако такое встречается довольно редко. Во всяком случае, обычные наследники TComponent не имеют счетчика ссылок, а _AddRef и _Release ничего не делают и всегда возвращают -1 (25) . А вот с наследниками TInterfacedObject следует быть осторожным…
- Существует опасность использование интерфейса объекта, который был удален. Например, приложение запрашивает у plugin'а какой-либо интерфейс, и начинает с ним работать. А plugin, как последняя редиска, вдруг выгружается. В итоге у приложения в руках оказывается ссылка на VMT, которой в действительности уже нету. Естественно, это ошибка программиста и за этим нужно следить.
- После того как интерфейс описан и начал использоваться - он не подлежит изменению (26). Если что-то нужно к нему добавить, следует сделать его наследника.
- Эту методику можно и в случае обыкновенных dll. Только (при компиляции dll без пакетов) следует иметь ввиду, что глобальный объект Application у приложения и dll будут разными и для доступа к главной форме из dll в последнюю надо будет передать Application из exe. При компиляции с VCL50 этого делать не нужно.
- Наверное, существует что-то еще (что определиться опытным путем в дальнейшем или умные люди подскажут …
Дмитрий Полщанов
9 октября 2001 г
Специально для Королевства Delphi
Скачать архив: Plugins01.zip (92 K)
(1) — кстати, компиляция приложения и dll с пакетом vcl50 полностью снимает проблему дочерних окон и использование менеджера памяти BORLNDMM.DLL (подключение ShareMem) становится не только не нужным, но и опасным .[Вернуться]
(2) — как ни странно, но и от Microsoft бывает что-нибудь хорошее… [Вернуться]
(3) — когда одно приложение управляет другим или позволяет себе "уведомлять"другое приложение о своих событиях[Вернуться]
(4) — когда приложения выполняются на разных компьютерах в локальной сети (или internet) и взаимодействуют друг с другом.[Вернуться]
(5) — подробнее о VMT будет рассказано далее.[Вернуться]
(6) — если произойдет "сдвиг" VMT, последствия могут быть непредсказуемыми -но фактический вызов будет неверным.[Вернуться]
(7) — когда один пакет ссылается на другой, а тот, в свою очередь, на третий, а тот …[Вернуться]
(8) — только выполняют при загрузке дополнительную работу[Вернуться]
(9) — кстати, использование TActionList довольно хорошая практика[Вернуться]
(10) — New/Package [Вернуться]
(11) — объектно-ориентированное программирование [Вернуться]
(12) — так называемый GUID - глобальный уникальный идентификатор [Вернуться]
(13) — я всегда испытываю некоторый скепсис, когда слышу про то, что чего-то "нам надолго хватит". В памяти еще остались ощущения от программирования под 16 разрядные среды (DOS и Windows) с 64k барьером и 640k общей памяти (которой, по тогдашнему мнению Microsoft, должно было хватить надолго). Но, как мне кажется, возможностей GUID, по крайней мере, на наш век хватит. Тем более в пределах одного приложения. [Вернуться]
(14) — функция QueryInterface более подробно обсуждается далее [Вернуться]
(15) — кстати, возникновение такой ситуации свидетельствует о плохом проектировании программы и следует пересмотреть всю идеологию системы в целом. [Вернуться]
(16) — в виду отсутствия в Delphi механизма множественного наследования, агрегация (то есть включение одного объекта в другой с транспортацией его свойств и методов в объект-контейнер) довольно часто применяется в Delphi. [Вернуться]
(17) — в частности, при переводе компонента в элемент ActiveX [Вернуться]
(18) — вместе с _AddRef и _Release она реализует интерфейс IUnknown, являющийся базовым интерфейсом для всех остальных интерфейсов (как TObject является базовым классом для всех классов Delphi). Для любителей порассуждать о том, какой язык лучше вот информация к размышлению: для реализации механизма интерфейсов (да и, пожалуй, полностью всего COM) в C++ (причем в классической реализации по Страуструпу) не нужно ничего, кроме быстрых и умелых рук. Тогда как в Delphi Object Pascal потребовалось для его поддержки пришлось вносить изменения в сам язык. [Вернуться]
(19) — допустим, IUnknown [Вернуться]
(20) — включая получение интерфейса IUnknown. [Вернуться]
(21) — но следует более тщательней подходить к реализации пакета базовых классов. То есть нужно постараться предусмотреть все, что только можно, ибо после запуска проекта в самостоятельное плавание малейшее изменение в этом пакете может потребовать перекомпиляцию, причем как основного приложения, так и всех без исключения plugin's [Вернуться]
(22) — правда, особого смысла я в этом не вижу [Вернуться]
(23) — а вот это может оказаться полезным (одна интеграция с MS Office'ом стоит многого) [Вернуться]
(24) — Библиотека поддержки COM в Delphi это использует на полную катушку[Вернуться]
(25) — пока не будет проинициализировано внутренне поле FVCLComObject TComponent. А это случится только при создании на основе TComponent ActiveX объекта. [Вернуться]
(26) — если вы не хотите все испортить :) [Вернуться]
[TInterfacedObject] [Использование пакетов (BPL)] [Модель плагинов]
Обсуждение материала [ 28-03-2010 21:35 ] 34 сообщения |