Дмитрий Кузан дата публикации 27-02-2008 07:17 Программа из кирпичиков, или плагины, плагины и еще раз плагины
В жизни каждого программиста наступает момент, когда приходит понимание необходимости создания модульных программ. То есть программ, обладающих гибкостью в расширении функциональности. Особенно это проявляется при создании корпоративных приложений. Недостаточная гибкость и неспособность к быстрому расширению функциональности системы влекут за собой задержки с выходом обновлений и, как следствие, затягивание сроков. Прибавим также и то, что часто на местах, куда поставляется программа, есть свои отделы АСУ, и в них (в большинстве) есть толковые программисты, которые, не дожидаясь исправлений и обновлений, начинают плодить собственные системы для решения круга задач, не предусмотренных программой. Зачастую эти разнообразные программы в массе своей представляют модули дополнительной отчетности, предназначенные для вывода на печать каких-то результатов. В итоге на предприятиях вместо одной функциональной системы появляется кучка программ, которые в принципе выполняют одни и те же задачи.
Что можно противопоставить данной тенденции — только открытую архитектуру и возможность не зависимым разработчикам (читай пользователям) вносить собственные дополнения и расширения в программу.
В данной статье я хочу привести пример создания простейшей модульной системы. В основе она будет состоять из БД (FireBird), основной программы (работа с БД через FibPlus), поддерживающей плагины, и дополнительных модулей, расширяющих функциональность основного блока (отчеты в данном случае сделаны на основе FastReport). Реализацию плагинов я осуществлю, опираясь на технологию COM — Component Object Model.
На Королевстве уже были статьи по разработке плагинов, но в этих статьях плагины реализовывались либо в виде DLL, либо в виде пакетов BPL. Оба подхода имеют как свои достоинства, так свои и недостатки. В свое время я перепробовал и то и другое, но мне все время что-то не нравилось, пока я не перешел на COM, на которой и остановился, ощутив все её прелести.
Сразу оговорюсь, плагины будут представлять собой внутренние COM-сервера, вызываемые (подгружаемые) из основной программы. Плагины будут представлять собой COM сервера без библиотеки типов. То есть они будут привязана к конкретному языку программирования (в нашем случае к Delphi) и будут зависимы от модуля описаний интерфейсов. Как же так, возразит дотошный читатель ведь тогда теряется универсальность, нельзя будет написать плагин на другом отличном от Delphi языке программирования. Да, это так, но ничто не мешает читателю самому создать плагин с независимой от языка системой определения интерфейсов, в виде библиотеки типов используя те же механизмы COM.
В нашем же случае я постараюсь превратить этот недостаток в этакое достоинство, хотя бы потому что в данном случае мы можем не зависеть от узкого набора параметров функций и процедур, представленных технологией COM серверам с библиотеками типов. То есть мы можем передавать в наши функции и процедуры что угодно — начиная с простых типов данных, массивов и заканчивая экземплярами объектов и т.п. Стоит отметить, что для реализации подобной передачи нестандартных данных в сервере с поддержкой библиотеки типов нужно написать собственную маршрутизацию, а это уже не так просто.
Итак, начнем. В качестве тестовой БД для нашего примера была разработана простенькая БД, имеющая следующую структуру (см. рис. 1)
Рис. 1
Как видите, БД действительно очень проста и будет нам служить основой для выдачи отчетов.
Перед созданием программы нужно продумать интерфейсы взаимодействия основного модуля и плагина. Данный этап очень важен, и им нельзя пренебрегать. Запомните: от четкого планирования структуры зависит та функциональная гибкость, которую Вы вложите в систему.
Так как наш проект касается в основном вопросам динамического подключения отчетности, то я сформулировал несколько требований к функциональности:
- Основной модуль должен самостоятельно загружать плагины при запуске системы из определенного каталога (в нашем случае каталог PlugIns)
- Основной модуль должен определять, является ли загружаемый плагин родным для него. Что я хочу этим сказать: плагины могут выполнять разные функции, и если плагин по функциональности не тот, то он должен отбрасываться или задействоваться в другом функционале. Сразу оговорюсь, функционал будет определяться через интерфейсы, и по наличию того или иного интерфейса будет происходить действие.
- Основной модуль должен определять, является ли загружаемый плагин родным для самого модуля. Проверка на тот случай, если в каталог плагинов подкинут плагин от другой системы, или файл вообще не является плагином. Короче, защита от дурака.
- Так как используется технология COM, основной модуль должен уметь регистрировать плагины в системе (прописывать их). Ключевое требование COM к серверам — они должны быть прописаны в системе.
- Основной модуль должен предоставлять плагину различную служебную информацию. Что имеется ввиду — плагин может запросить из программы: например, хендл окна, или хендл соединения с БД, или еще что-нибудь. Основной модуль должен это обеспечивать. Вывод: основной модуль должен сам выступать в роли 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 нашего задания:
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
Function Get_AppHandle : THandle;
Function Get_DBHandle : TISC_DB_HANDLE;
Function Get_PathShablon : String;
end;
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;
hndDLLHandle := loadLibrary ( PlugInName );
if hndDLLHandle <> 0 then
begin
func_IsPlugins := getProcAddress ( hndDLLHandle, 'IsPlugins' );
if Addr (func_IsPlugins) <> nil then
begin
func_GetCLSID := getProcAddress ( hndDLLHandle, 'GetCLSID' );
IF Assigned(func_GetCLSID) then
begin
PlgCLSID := func_GetCLSID;
WriteLog (tdsMessage, Format(' Определен CLSID: %S.',[ GUIDToString(PlgCLSID)]) );
CheckComServerInstalled(PlgCLSID, String(PlugInName));
_IReportPlugin := nil;
_IReportPlugin := CreateComObject(PlgCLSID) as 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
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;
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;
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
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;
_MainManager := CreateComObject(Class_TMainManager) as IMainManager;
if _MainManager <> nil then
begin
Application.Handle := _MainManager.Get_AppHandle;
frmRepForm := TfrmRepForm.Create(Application);
DMRep := TDMRep.Create(Application);
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. Дизайнер отчета
В окончании этой статьи подведу итоги. Использование интерфейсов предоставляет в руки программиста неограниченный объем свободы в сборке программ по кирпичикам. Дерзайте.
К материалу прилагаются файлы:- Демо-проект (3542 K) обновление от 2/27/2008 7:33:00 AM
Обсуждение материала [ 02-08-2011 12:36 ] 15 сообщений |