Rambler's Top100
"Knowledge itself is power"
F.Bacon
Поиск | Карта сайта | Помощь | О проекте | ТТХ  
 Подземелье Магов
  
 

Фильтр по датам

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  02:19[Войти] | [Зарегистрироваться]

Программа из кирпичиков, или плагины, плагины и еще раз плагины

Дмитрий Кузан
дата публикации 27-02-2008 07:17

Программа из кирпичиков, или плагины, плагины и еще раз плагины

В жизни каждого программиста наступает момент, когда приходит понимание необходимости создания модульных программ. То есть программ, обладающих гибкостью в расширении функциональности. Особенно это проявляется при создании корпоративных приложений. Недостаточная гибкость и неспособность к быстрому расширению функциональности системы влекут за собой задержки с выходом обновлений и, как следствие, затягивание сроков. Прибавим также и то, что часто на местах, куда поставляется программа, есть свои отделы АСУ, и в них (в большинстве) есть толковые программисты, которые, не дожидаясь исправлений и обновлений, начинают плодить собственные системы для решения круга задач, не предусмотренных программой. Зачастую эти разнообразные программы в массе своей представляют модули дополнительной отчетности, предназначенные для вывода на печать каких-то результатов. В итоге на предприятиях вместо одной функциональной системы появляется кучка программ, которые в принципе выполняют одни и те же задачи.

Что можно противопоставить данной тенденции — только открытую архитектуру и возможность не зависимым разработчикам (читай пользователям) вносить собственные дополнения и расширения в программу.

В данной статье я хочу привести пример создания простейшей модульной системы. В основе она будет состоять из БД (FireBird), основной программы (работа с БД через FibPlus), поддерживающей плагины, и дополнительных модулей, расширяющих функциональность основного блока (отчеты в данном случае сделаны на основе FastReport). Реализацию плагинов я осуществлю, опираясь на технологию COM — Component Object Model.

На Королевстве уже были статьи по разработке плагинов, но в этих статьях плагины реализовывались либо в виде DLL, либо в виде пакетов BPL. Оба подхода имеют как свои достоинства, так свои и недостатки. В свое время я перепробовал и то и другое, но мне все время что-то не нравилось, пока я не перешел на COM, на которой и остановился, ощутив все её прелести.

Сразу оговорюсь, плагины будут представлять собой внутренние COM-сервера, вызываемые (подгружаемые) из основной программы. Плагины будут представлять собой COM сервера без библиотеки типов. То есть они будут привязана к конкретному языку программирования (в нашем случае к Delphi) и будут зависимы от модуля описаний интерфейсов. Как же так, возразит дотошный читатель ведь тогда теряется универсальность, нельзя будет написать плагин на другом отличном от Delphi языке программирования. Да, это так, но ничто не мешает читателю самому создать плагин с независимой от языка системой определения интерфейсов, в виде библиотеки типов используя те же механизмы COM.

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

Итак, начнем. В качестве тестовой БД для нашего примера была разработана простенькая БД, имеющая следующую структуру (см. рис. 1)


Рис. 1

Как видите, БД действительно очень проста и будет нам служить основой для выдачи отчетов.

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

Так как наш проект касается в основном вопросам динамического подключения отчетности, то я сформулировал несколько требований к функциональности:

  1. Основной модуль должен самостоятельно загружать плагины при запуске системы из определенного каталога (в нашем случае каталог PlugIns)
  2. Основной модуль должен определять, является ли загружаемый плагин родным для него. Что я хочу этим сказать: плагины могут выполнять разные функции, и если плагин по функциональности не тот, то он должен отбрасываться или задействоваться в другом функционале. Сразу оговорюсь, функционал будет определяться через интерфейсы, и по наличию того или иного интерфейса будет происходить действие.
  3. Основной модуль должен определять, является ли загружаемый плагин родным для самого модуля. Проверка на тот случай, если в каталог плагинов подкинут плагин от другой системы, или файл вообще не является плагином. Короче, защита от дурака.
  4. Так как используется технология COM, основной модуль должен уметь регистрировать плагины в системе (прописывать их). Ключевое требование COM к серверам — они должны быть прописаны в системе.
  5. Основной модуль должен предоставлять плагину различную служебную информацию. Что имеется ввиду — плагин может запросить из программы: например, хендл окна, или хендл соединения с БД, или еще что-нибудь. Основной модуль должен это обеспечивать. Вывод: основной модуль должен сам выступать в роли COM-сервера и иметь декларируемые интерфейсы, предоставленные разработчику плагина для получения этой информации.

Примечание: Данные интерфейсы после выхода в свет не должны более изменятся. Это ключевое требование COM. Если вы хотите расширить в будущем функциональность — наследуйте интерфейсы и создавайте дополнительную реализацию. Старая реализация должна также присутствовать для совместимости.

Плагин для простоты нашего примера должен поддерживать следующую функциональность:

  • уметь отображать окно предварительной настройки отчета;
  • уметь выводить предварительный просмотр;
  • выдавать название отчета по запросу из основного модуля;
  • иметь свойство, по которому можно определить, возможно или нет редактирование шаблона отчета
  • уметь вызывать дизайнер при данной возможности.

Итак, требования сформулированы, приступим к реализации — для начала создадим unit, в котором опишем наши интерфейсы. Данный модуль мы будем предоставлять разработчикам плагинов как SDK к нашей программе. Unit я назвал Interface_Libray.pas.

Первое, опишем декларацию основного интерфейса плагина —

  IReportPlugin = Interface
             ['{8BBACCD4-8A6E-4577-8805-429A2A858A3D}']
             // Можно или нет редактировать шаблон
             Function  IsDesign : Boolean;
             // Вывести окно настроек 
             Function  Execute  : Boolean;
             Procedure Preview; // предварительный просмотр печати
             Procedure Designer;// Вызов дизайнера
             // Название отчета
             Function  GetName  : TCaption;
  end;

Вторым действие опишем типы функций, обязательные в DLL для реализации требований 3 и 4 нашего задания:

// Описываем функции DLL
  TIsPlugins        = function : Boolean;
  TGetCLSID         = function : TGUID;

Первая функция в dll будет нам говорить, что плагин наш. То есть если данная функция присутствует в DLL плагина, то плагин родной для нашего основного модуля. Вторая функция будет возвращать CLSID (GUID внутреннего COM-сервера) плагина нашему модулю. Далее будет это подробно расписано.

Вторым нашим действием станет описания интерфейсов основного модуля для взаимодействия с плагином. Я опишу его для простоты так.

  IMainManager = Interface
            ['{6DB2FF6E-5FCE-484E-BFB9-1E81935FE844}']
                   // Функция возвращает хендл приложени
                   Function  Get_AppHandle         : THandle;
                   // Функция возвращает хендл соединения с БД
                   Function  Get_DBHandle          : TISC_DB_HANDLE;
                   // Функция возвращает путь к шаблонам отчетов
                   Function  Get_PathShablon       : String;
  end;

Также выведем в отдельную константу CLSID класса, реализующего данный интерфейс в основном модуле. Это сделано для того, что бы разработчик плагина знал CLSID сервера и мог его вызвать в своем плагине.

const
  // Класс Менеджера
  Class_TMainManager : TGUID = '{2C905A01-D678-4508-AE0A-87CCC011FB65}';

Итак, основные интерфейсы описаны, приступим к реализации основного модуля:

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

program MainProject;

uses
  ShareMem,
…

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

// Основной менеджер
  TMainManager = class(TComObject, IMainManager)
  protected
    //******************************************************************
    // Описание функций интерфеса  IMainManager
    //******************************************************************
    // Функция возвращает хендл приложени
    Function  Get_AppHandle         : THandle;
    // Функция возвращает хендл соединения с БД
    Function  Get_DBHandle          : TISC_DB_HANDLE;
    // Функция возвращает путь к шаблонам отчетов
    Function  Get_PathShablon       : String;
  end;

{ TMainManager }
// РЕАЛИЗАЦИЯ 
function TMainManager.Get_AppHandle: THandle;
begin
 Result := Application.Handle;
end;

function TMainManager.Get_DBHandle: TISC_DB_HANDLE;
begin
 Result := frmMain.FIBDB.Handle;
end;

function TMainManager.Get_PathShablon: String;
begin
 Result := ExtractFilePath(ParamStr(0));
 IF Result[Length(Result)]<> '\' then  Result := Result + '\';
 Result := Result + 'SHABLON\';
end;

В раздел инициализации добавим создание фабрики класса:

initialization
  // Создаем фабрику классов реализующий интерфейс менеджера
  TComObjectFactory.Create(ComServer, TMainManager, Class_TMainManager,
                          'MainManager', '',
                          ciMultiInstance, tmApartment);

Обратите внимание на константу Class_TmainManager, которую мы объявили ранее. Обеспечим создание экземпляра класса в FormCreate и перейдем к реализации механизма автоматической загрузки плагинов. Для этого я создал функции LoadPlugIns и LoadPlugIn. Первая сканирует каталог PLUGINS, вторая добавляем информацию о найденном файле плагина в коллекцию плагинов (описание см. в файле_Class_ReportsList.pas)

// Загрузка плагинов **************************************************
procedure TfrmMain.LoadPlugIns;
var
  PathPlgIns  : String;
  SearchRec   : TSearchRec ;
  I           : Integer;
begin
  PathPlgIns := ExtractFilePath(ParamStr(0)) + 'PLUGINS\';
  FindFirst(PathPlgIns + '*.dll' , faAnyFile, SearchRec);
  Repeat
    IF (SearchRec.Name <> ''  ) and  (SearchRec.Name <> '..') and
       (SearchRec.Name <> '.' ) and  ((SearchRec.Attr and faDirectory) <> faDirectory)
    Then
    Begin
       WriteLog (tdsMessage, Format('Попытка загрузки: %S ',[SearchRec.Name]) );
       LoadPlugIn(PAnsiChar(PathPlgIns + SearchRec.Name));
    End;
  Until FindNext(SearchRec) <> 0;

  // После того как мы загрузили плагины строим список отчетов
  for I := 0 to RepManager.Reports.Count-1 do
  begin
      ListBoxRep.Items.Add( RepManager.Reports.Items[0].Caption );
  end;
end;

//----------------------------------------------------------------------
Function TfrmMain.LoadPlugIn(PlugInName : PAnsiChar) : Boolean;
var
  hndDLLHandle     : THandle;
  func_IsPlugins   : TIsPlugins;
  func_GetCLSID    : TGetCLSID;
  PlgCLSID         : TGUID;
  _IReportPlugin   : IReportPlugin;
  Item             : TReportItem;      // Элемент
begin
  try
    Result := False;
    // загружаем dll динамически
    hndDLLHandle := loadLibrary ( PlugInName );
    if hndDLLHandle <> 0 then
    begin
       // получаем адрес функции
       func_IsPlugins := getProcAddress ( hndDLLHandle, 'IsPlugins' );
       // если адрес функции найден
       if Addr (func_IsPlugins) <> nil then
       begin
          {Есть плагин - запрашиваем CLSID}
          func_GetCLSID := getProcAddress ( hndDLLHandle, 'GetCLSID' );
          IF Assigned(func_GetCLSID) then
          begin
             // Получаем CLSID c DLL
             PlgCLSID := func_GetCLSID;
             WriteLog (tdsMessage, Format(' Определен CLSID: %S.',[ GUIDToString(PlgCLSID)]) );
             // Регистрируем сервер
             CheckComServerInstalled(PlgCLSID, String(PlugInName));

             // Создаем сервер - загружаем его
             _IReportPlugin := nil;
             _IReportPlugin := CreateComObject(PlgCLSID) as IReportPlugin;
             // Проверка на поддержку функциональности интерфейса IReportPlugin
             if _IReportPlugin <> nil then
             begin
                  WriteLog (tdsMessage, Format(' Имя плагина: %S.',[ _IReportPlugin.GetName ]) );

                  // Добавляем плагин в коллекцию
                  Item := RepManager.AddReport;
                  Item.Caption := _IReportPlugin.GetName;
                  Item.DLLName := PlugInName;
                  Item.CLSID   := PlgCLSID;
                  Item.IsDesigner := _IReportPlugin.IsDesign;
             end
             else
                 WriteLog (tdsError, Format('Плагин %S не содержит требуемую функциональность',[PlugInName]) );
          end
          else
             WriteLog (tdsError, Format('Плагин %S не содержит информацию о CLSID.',[PlugInName]) );
       end
       else
          WriteLog (tdsError, Format('Плагин %S не является плагином.',[PlugInName]) );
    end
    else
       WriteLog (tdsError, Format('Не могу загрузить плагин %S.',[PlugInName]) );
   finally
      // liberar
      freeLibrary ( hndDLLHandle );
   end;
end;

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

procedure TfrmMain.act_ExecuteExecute(Sender: TObject);
var
 _IReportPlugin   : IReportPlugin;
 Item             : TReportItem;      // Элемент
begin
 Item := RepManager.GetItem(ListBoxRep.ItemIndex);
 if Item <> nil then
    _IReportPlugin := CreateComObject(Item.CLSID) as IReportPlugin;
    // Проверка на поддержку функциональности интерфейса IReportPlugin
    if _IReportPlugin <> nil then
    begin
       IF _IReportPlugin.Execute Then
          _IReportPlugin.Preview;
    end;
end;

procedure TfrmMain.act_DesignerExecute(Sender: TObject);
var
 _IReportPlugin   : IReportPlugin;
 Item             : TReportItem;      // Элемент
begin
 Item := RepManager.GetItem(ListBoxRep.ItemIndex);
 if Item <> nil then
    _IReportPlugin := CreateComObject(Item.CLSID) as IReportPlugin;
    // Проверка на поддержку функциональности интерфейса IReportPlugin
    if _IReportPlugin <> nil then
    begin
       _IReportPlugin.Designer;
    end;
end;

Полный исходный код основного модуля вы можете посмотреть в папке MainProg. На этом, можно сказать, наш основной модуль готов.

Приступим теперь к реализации самого плагина. Для этого создадим внутренний COM-сервер с помощью мастера COM Wizard (см. рис. 2).


Рис. 2

Далее в файле проекта dll опишем функции

function IsPlugins: Boolean;
  begin
     Result := true;
  end;

  function GetCLSID : TGUID;
  begin
     Result := PlugInInterface.Class_TPlugIn;
  end;

И добавим их в раздел Export нашей DLL.

exports
  DllGetClassObject,
  DllCanUnloadNow,
  DllRegisterServer,
  DllUnregisterServer,
  IsPlugins, {Обязательно экспортируем эти функции}
  GetCLSID;

Обратите внимание на PlugInInterface.Class_TplugIn — в данной константе я храню CLSID COM-сервера плагина, реализующего интерфейс IreportPlugin. Рекомендую Вам сделать заготовки констант CLSID и ClassName на будущее, как сделал я:

const                                                         
  // УНИКАЛЬНЫЙ ИДЕНТИФИКАТОР КЛАССА ПЛАГИНА
  Class_TPlugIn: TGUID = '{D4C3FE84-E686-44A2-978E-BC1E7C9A137D}';
  Class_Name           = 'DemoPlugIns';

Это позволит вам в будущем менять CLSID и имя класса для будущих плагинов.

В секцию инициализации добавим создание фабрики классов:

initialization
  TComObjectFactory.Create(ComServer, TPlugIn, Class_TPlugIn,
                           Class_Name, '', ciSingleInstance, tmApartment);

Обратите внимание на ciSingleInstance — для внутренних COM-серверов нужно использовать только одиночный экземпляр класса.

Реализуем интерфейс IreportPlugin в плагине:

TPlugIn = class(TComObject, IReportPlugin)
  private
    _MainManager : IMainManager; // Интерфейс основного модуля
    SaveHandle   : THandle;      // хендл основного приложения
    PathShablon  : String;
    …
  protected
    // *************************************************************************
    // ИНТЕРФЕЙСНАЯ ЧАСТЬ IReportPlugin
    // Обязательная для всех плагинев *******************************************
    // Можно или нет редактировать шаблон
    Function  IsDesign : Boolean;
    // Вывести окно настроек
    Function  Execute  : Boolean;
    Procedure Preview; // предварительный просмотр печати
    Procedure Designer;// Вызов дизайнера
    // Название отчета
    Function  GetName  : TCaption;
  Published
    {Конструктор + деструктор}
    procedure  Initialize; override;
    destructor Destroy; override;
  end;

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

// Первичная инициализация
procedure TPlugIn.Initialize;
begin
  inherited;
  SaveHandle := Application.Handle;       // сохраняем хэндл dll

  // Получаем интерфейс менеджера плагинов
  _MainManager := CreateComObject(Class_TMainManager) as IMainManager;
  if _MainManager <> nil then
  begin
      // Заменяем хэндл DLL хэндлом вызывающего приложения
      Application.Handle := _MainManager.Get_AppHandle;
      // Создаем окно настроек
      frmRepForm := TfrmRepForm.Create(Application);
      DMRep      := TDMRep.Create(Application);
      // restore
      Application.Handle := SaveHandle;

      // Устанавливаем связь с БД
      IF _MainManager.Get_DBHandle <> nil then
      begin
         With DMRep do begin
           FIBbase.Handle := _MainManager.Get_DBHandle;
           FIBbase.Open;
         end;
      end;

      frmRepForm.Caption := 'Отчет: ' + GetName;
      frmRepForm.Update;

      // Запрашиваем путь к шаблонам
      PathShablon := _MainManager.Get_PathShablon;
   end;
end;

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

Далее я реализую необходимую функциональность в плагине по работе с отчетом. Для примера я запрашиваю данные с БД и подготавливаю набор данных для вывода в отчет:

// подготовка рабочих наборов
procedure TPlugIn.Preparing(IsDesig : Boolean);
var
 Ds : TFIBDataSet;
begin
 With DMRep do
 begin
  try
   frxReport.LoadFromFile(PathShablon + ShablonName);
   Zapr.Close;
   Zapr.SQLs.SelectSQL.Clear;
   Zapr.SQLs.SelectSQL.Add('Select S.FAMILY, S.NAME, O.NAME, K.RAB from KADR K');
   Zapr.SQLs.SelectSQL.Add('LEFT JOIN SOTRUD S on (K.ID_SOTRUD = S.ID)        ');
   Zapr.SQLs.SelectSQL.Add('LEFT JOIN OTDELS O on (K.ID_OTDEL = O.ID)         ');
   Zapr.Open;
  except
  end;
 end;
end;

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

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


Рис. 3. Выполнение отчета и вывод диалогового окна предварительной настройки параметров отчета. Так как у нас демо-пример, то выводим просто окошечко. Если для отчета не нужно диалоговое окно, то просто убираем функциональность в методе Execute

Рис. 4. Выполненный отчет из плагина. Данные взяты с БД

Рис. 5. Дизайнер отчета

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



К материалу прилагаются файлы:


Смотрите также материалы по темам:


 Обсуждение материала [ 02-08-2011 12:36 ] 15 сообщений
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования