Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Немного о плагинах

- Мда... не всегда получается как хотелось бы: купил, вставил и заработало.
- Да, даже plug&play в жизни не всегда работает.

Введение

Проблема декомпозиции приложения рано или поздно возникает в любом серьезном проекте. Цели декомпозиции могут быть различны, но можно выделить наиболее часто встречающиеся:

Облегчение сопровождения. Приложение разбивается на функциональные модули так, чтобы можно было без опаски заменить "ошибочный" модуль на (якобы) "исправленный" 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 (при этом не таская за собой ее громоздкую библиотеку поддержки); и во-вторых, предложить небольшую модернизацию метода Игоря Трофимова;

Предлагаемая здесь методика опробирована на рабочем проекте с достаточно приемлимым результатом.

Часть 1.

Давайте сделаем базовый проект, обеспечивающий динамическую подгрузку пакета. На самом деле это достаточно тривиально, но нам необходимо начать с чего-нибудь привычного (а кому это не привычно - он прочувствует, что это не так сложно, как это кажется на первый взгляд) просто иметь некоторую стартовую точку показать, что и разработанные на текущий момент проекты могут быть легко модернизированы с учетом данной методики (возможно, их придется переписывать заново (или начисто, в зависимости от того, как к этому относиться!), - но это даже иногда полезно ).

Для начала спроектируем первое приближение главного приложения. Я хочу показать использование как диалоговых, так и дочерних окон, поэтому главное окно приложения сделаем 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;


Теперь что мы имеем.

Во-первых, мы не знаем действительный тип как дочернего, так и диалогового окон. Но между тем вызываем функции, входящие в его 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;

implementationprocedure 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, или где-нибудь еще). Мы просто обнар ужили, что есть какая-то активная форма, спросили ее на предмет поддержки конкретного интерфейса и вызвали его метод.

Часть 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);
  privateprocedure 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).

Часть 5. Агрегация (16)

До сих пор для получения интерфейса объекта я использовал функцию GetInterface. Она прекрасно работает и удобна в использовании, но имеет существенные ограничения. Прежде всего, эта функция не виртуальная. То есть, вы не сможете переопределить ее поведени е в классах-наследниках. А делает эта функция только одно - сканирует локальную VMT объекта на предмет получения требуемого куска VMT. Однако, начиная от TComponent, компоненты Delphi содержат функцию, делающую почти то же самое, но являющуюся виртуальной . Под "почти" я имею ввиду то, что эта функция вызывает GetInterface, но осуществляет еще дополнительные проверки и имеет немного другой формат вызова. Эта функция в последствии (17) принимает участие в COM программировании и имеет наименование QueryInter face (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 в последнюю надо будет передать Applicatio n из exe. При компиляции с VCL50 этого делать не нужно. Наверное, существует что-то еще (что определиться опытным путем в дальнейшем или умные люди подскажут …

Проект Delphi World © Выпуск 2002 - 2024
Автор проекта: USU Software
Вы можете выкупить этот проект.