Интерфейсы и плагины

Интерфейсы и плагины ВведениеПожалуй, каждому программисту со временем приходит в голову мысль, что его замечательной программе не хватает гибкости. И действительно, допустим, есть большая программа, и в нее понадобилось внести небольшие изменения. Не важно, по какой причине. Но для этого ее надо откомпилировать, проверить, а потом еще и поставить на рабочие места. Нет нужды говорить, что это достаточно долгое занятие. В другом случае может возникнуть желание, чтобы другие программисты могли расширять функциональность программы, а когда программа - единое целое, то им надо полностью поставить исходный код, еще и с описанием, где что находится, какие библиотеки нужны и так далее.И программист приходит к выводу, что очень желательно, чтобы его программа состояла из набора отдельных модулей (плагинов), каждый из которых был бы независим от других. Тогда при необходимости можно изменить и отладить только один модуль, а сторонним программистам дать шаблон интерфейса. Программ, построенных таким образом, множество, и остается только завидовать. Чаще всего модульность реализуется на основе механизмов dll, динамических библиотек. Вот только со стандартной dll возникают трудности: экспортируются только процедуры, да и ansistring не так просто передать. А хочется ведь передавать в модули и объекты. Конечно, это возможно, но... С этого момента плагины можно писать только на Delphi, и, мало того, именно на той версии, с которой начиналась разработка, никто ведь не гарантирует, что внутреннее представление объекта не поменяется с изменением версии среды разработки. И, как ни странно, мало кто обращает внимание на механизм, который есть в любой современной версии Windows, и который предназначен прежде всего именно для взаимодействия модулей и программ. Я имею в виду COM. И если учесть, что в Delphi работа с COM является достаточно простым делом, очень странным кажется тот факт, что в большинстве случаев применение этой технологии ограничивается, например, выдачей в Excel отчетов или работой с TWebBrowser.Но эта технология позволяет легко создавать модульные приложения, и с достаточно низкими затратами. Все, что нужно - реализовать интерфейсы к объектам, и обмениваться этими интерфейсами. И совершенно не важно, на какой версии Delphi написан плагин, фактически, он может быть написан вообще на другом языке программирования: COM не зависит от языка. При этом СОМ - объектная технология, тесно интегрированная в Delphi.Я хочу показать, как можно с помощью COM решить задачу создания плагинов, маленьких частей программы, которые могут быть изменены независимо от основной программы. В частности, здесь я рассматриваю, как создать несколько библиотек, по-разному реализующих одну и ту же функциональность. Итак, что же такое интерфейсы и с чем их едят? Говоря простым языком, интерфейс COM - это всего лишь структура, которая содержит объявления методов. И не более того. Ближайший аналог в Delphi - объявление класса.Для создания плагинов нужно понимать, что у интерфейса нет экземпляра, это просто декларация, которая может быть понята на любом языке программирования, в котором есть возможность работы с COM. Собственно код и данные должны содержаться в объекте, который реализует этот интерфейс, для него даже есть специальное название, кокласс (coclass). И может быть сколько угодно коклассов, реализующих один и тот же интерфейс. Единый интерфейс с различным наполнением - как раз то, что нужно.То есть, нужно просто создать ту реализацию интерфейса, которая нужна, а дальше просто общаться с ней через интерфейс, который везде одинаков.Давайте рассмотрим конкретный пример. Для этого я взял три метода сортировки массива, которые нагло утянул из проекта в demos\threads. Это BubbleSort, SelectionSort и QuickSort. Потоки и визуализацию рассматривать не будем, по крайней мере, сейчас. Задача стоит в том, чтобы создать три плагина для сортировки массива, реализующих каждый свой метод, и подключать один из них по мере надобности. Разумеется, каждая реализация должна находиться в своей библиотеке (dll).Объекты СОМСначала рассмотрим простейший способ.Как можно догадаться, прежде всего нам понадобится интерфейс. Затем - библиотека, содержащая его реализацию. Я предлагаю начать с библиотеки, так удобнее. При этом я не буду выдавать подробные инструкции вроде "мееедленно подведите указатель мышки к пункту меню File...", я просто покажу, что нужно:clip0034 В технологиии COM используются библиотеки специального вида, и при выборе ActiveX Library как раз создается шаблон такой библиотеки. Далее, сразу создаем COM Object с этой же вкладки, при этом среда спрашивает, что именно нужно. Вот это:clip0035 Можно догадаться, что первым будет реализован метод пузырька. Обратите внимание, что Type Library не нужна. В библиотеку добавляется модуль, в котором уже объявлен класс, потомок TComObject. Не хватает только интерфейса. И, поскольку библиотеки типов нет, нужно объявить его вручную, и, желательно, в отдельном модуле для дальнейшего использования. Но предварительно надо сохранить то, что получилось. Я дал название библиотеке BubbleLib, а модулю кокласса - CBubble. Теперь добавим модуль с интерфейсом:

unit SortIntf;
interface
type
 ISortIntf = interface
 ['{BE229ED0-BA7B-11DA-B665-00508B0973BE}']
  procedure Sort(var A: Variant); stdcall;
 end;
implementation
end.
Каждый интерфейс идентифицируется своим номером, GUID, который написан в первой его строке. Чтобы его получить, достаточно нажать Ctrl+Shift+G. При этом среда вызывает функцию CoCreateGUID и вставляет его в позицию курсора. Разумеется, при каждом новом вызове выдается новый GUID. Часто GUID интерфейса называют IID, идентификатор интерфейса. Это делается для того, чтобы отличать идентификатор интерфейса от, например, идентификатора кокласса, который обычно называют CLSID. Можно заметить, что в метод передается почему-то Variant, а никак не массив. К сожалению, здесь действуют те же правила, что и с обычной dll: передавать только безопасные типы данных. Вот я и собираюсь передавать Safe array.Теперь в модуле CBubble дописываем его реализацию (не забыв добавить SortIntf в секцию uses):
type
 TBubbleSort = class(TComObject, ISortIntf)
 protected
  procedure Sort(var A: Variant); stdcall;
 end;
И в разделе implementation:
uses ComServ, Variants;
{ TBubbleSort }
procedure TBubbleSort.Sort(var A: Variant);
var
 I, J, T: Integer;
 HighBound, LowBound: integer;
begin
 HighBound := VarArrayHighBound(A, 1);
 LowBound := VarArrayLowBound(A, 1);
 for I := HighBound downto LowBound do
  for J := LowBound to HighBound - 1 do
  if A[J] > A[J + 1] then
  begin
  T := A[J];
  A[J] := A[J + 1];
  A[J + 1] := T;
  end;
end;
Я не стал вызывать VarArrayLock, хотя это ускорило бы работу. Весь этот пример создается как простейшая иллюстрация создания и реализации интерфейсов, и не более того. После написания кода остается только вызвать Register ActiveX server для регистрации библиотеки как сервера COM. При этом в моей версии Delphi 6 почему-то создается файл BubbleLib_TLB.pas и в файл проекта записывается строка {$R *.tlb}. Одновременно создается библиотека типов, которая совершенно не нужна. Я так и не нашел причины, по которой она создается. Можно просто удалить файлы BubbleLib.tlb, BubbleLib_TLB.pas и убрать эту строку. Я так и сделал, причем перерегистрировать библиотеку не требуется. Регистрация требуется только если изменились идентификаторы интерфейсов или добавлены новые: все они должны быть записаны в реестре.Две оставшиеся библиотеки создаются абсолютно аналогично, только, разумеется, модуль SortIntf просто подключается к проекту. Для иллюстрации того, как можно использовать эти три компонента, я сделал маленькое приложение, которое назвал BubbleTest. Создал я его сразу после реализации первой библиотеки, а потом решил не менять название.Это обычное приложение с единственной формой, на которую я кинул TMemo, TSpinEdit, TListBox и кнопку. Мемо - для отображения массива, лист - для списка классов, ну а остальное - подручные стредства. Кроме этого, к проекту надо опять же подключить модуль SortIntf, чтобы программа знала, с каким интерфейсом придется работать. А для создания списка классов я просто перенес константы с CLSID (идентификаторами коклассов) из каждой библиотеки в модуль формы. После этого - несколько обработчиков:
const
 Class_BubbleSort: TGUID = '{CFB8F671-BA45-11DA-B665-00508B0973BE}';
 Class_SelectionSort: TGUID = '{BE229ED3-BA7B-11DA-B665-00508B0973BE}';
 Class_QuickSort: TGUID = '{BE229ED4-BA7B-11DA-B665-00508B0973BE}';
implementation
uses COMObj, SortIntf;
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
 Bubble: ISortIntf;
 i: integer;
 A: Variant;
 HighBound, LowBound: integer;
 Class_ID: TGUID;
 ElemCount: integer;
begin
 //Получаем нужный CLSID
 Class_ID := StringToGUID(ListBox1.Items[ListBox1.ItemIndex]);
 //Создаем класс по этому CLSID и сразу просим у него интерфейс ISortIntf
 Bubble := CreateCOMObject(Class_ID) as ISortIntf;
 ElemCount := SpinEdit1.Value;
 //Массив создаем и записываем в мемо
 A := VarArrayCreate([0,ElemCount-1], varInteger);
 Memo1.Lines.Clear;
 HighBound := VarArrayHighBound(A, 1);
 LowBound := VarArrayLowBound(A, 1);
 for i := LowBound to HighBound do
 begin
  A[i] := random(300);
  Memo1.Lines.Add(IntToStr(A[i]));
 end;
 //А вот, собственно, и вызов метода плагина
 Bubble.Sort(A);
 //Остается показать результат
 Memo1.Lines.Add('Sorted');
 HighBound := VarArrayHighBound(A, 1);
 LowBound := VarArrayLowBound(A, 1);
 for i := LowBound to HighBound do
 begin
  Memo1.Lines.Add(IntToStr(A[i]));
 end;
end;
procedure TForm1.FormShow(Sender: TObject);
begin
 with ListBox1.Items do
 begin
  Clear;
  Add(GUIDToString(Class_BubbleSort));
  Add(GUIDToString(Class_SelectionSort));
  Add(GUIDToString(Class_QuickSort));
 end;
 ListBox1.ItemIndex := 0;
end;
Сама форма у меня в результате получилась такая:clip0036 Теперь можно выбрать любую из трех строк в списке, и при нажатии на кнопку будет загружен нужный плагин, и вызван его метод Sort для сортировки случайного массива из 10 элементов. Собственно, это и все. Освобождать интерфейс явне не надо, при выходе из обработчика интерфейс будет освобожден, и библиотека с коклассом выгрузится. Если нужно освободить интерфейс явно, достаточно присвоить Bubble := nil (интерфейс - это указатель, почти) или просто присвоить ему другой интерфейс. Список CLSID я создал попутно просто для иллюстрации, как обращаться с GUID. Можно догадаться, что в этот список можно загрузить строки, например, из текстового файла, и копирование констант из библиотек прямо в код приложения будет не нужно.Теперь посмотрим, что получилось. На первый взгляд, сделанное очень мало отличается от использования трех обычных библиотек, экспортирующих каждая свой метод Sort. Однако, есть и преимущества: во-первых, для работы теста не нужно знать где находится библиотека и как она называется. Нужен только идентификатор нужного класса. Во-вторых, не нужно заботиться о загрузке и выгрузке этой библиотеки, можете быть уверены, что она загрузится когда будет нужна и выгрузится, когда все интерфейсы освободятся. В-третьих, все-таки мы работаем с объектом, хоть и содержащим только один метод. В-четвертых, не нужно заботиться об уничтожении созданного объекта, Delphi делает это автоматически. Далее я рассмотрю, как можно использовать объекты и интерфейсы более полноценным образом.АвтоматизацияРазумеется, нельзя не заметить, что все еще имеются некоторые тонкости в использовании плагинов. Все еще нужен модуль с описанием интерфейса, который используется везде, где есть этот интерфейс. Это неудобно тем, что при создании плагина на других языках это описание придется переводить. О CLSID я не говорю: хотя он нужен для создания класса, его всегда можно передать в виде строки. Наконец, въедливый программист может спросить об обработке ошибок в методе Sort. Что будет, если подать на вход не массив, а обычный variant, например, со строкой? Будет выдано исключение, из которого можно понять только его тип, модуль, в котором оно произошло, и адрес. Это не слишком удобно. Потому я хочу предложить сделать все по-другому. Помните, я в свое время говорил, что type library не нужна? Использовать ее конечно не обязательно, но очень удобно. Фактически, библиотека типов выполняет почти ту же роль, что и файл SortIntf.pas, но при этом содержит в себе еще и дополнительную информацию, например, описание кокласса. При этом все объявления в библиотеке типов не зависят от языка программирования. Кроме всего сказаного, в VCL содержится множество классов, работающих именно с библиотекой типов и обеспечивающих дополнительные функции, в том числе и обработку исключений.Но перед тем, как начинать работу с бибиотекой типов, рекомендуется все хорошо обдумать. Дело в том, что библиотека типов будет использоваться подобно SortIntf.pas, то есть везде, где нужно описание интерфейса. И в случае, когда потребуется изменить эти описания, потребуется повторное подключение этой библиотеки. Прежде всего, надо определить хотя бы примерный состав объектов, которые нужны. Например, в первой части происходила сортировка простого массива чисел. Но на практике сортируют обычно не просто числа, а записи или строки. Да что угодно, лишь бы эти объекты были сравнимы. Поэтому в метод сортировки лучше передавать не массив чисел, а массив объектов, которые должны просто знать, как сравнить себя с другим таким же объектом. Поскольку у нас COM, то вместо объектов будут использоваться, разумеется, интерфейсы.Что мне хочется: есть три метода сортировки, значит, надо создать три класса. Если каждый класс будет содержать методы работы с массивом, причем одинаковые, то весьма желательно выделить эту часть в отдельный модуль. Далее все три класса будут использовать то, что в нем есть. Таким образом, нам нужен интерфейс для представления массива и интерфейс для его сортировки.Начнем. По аналогии с созданием файла описания интерфейса создадим библиотеку типов. Отдельную от всего, в ней будут храниться только описания интерфейсов. Создать ее можно из той же вкладки ActiveX, элемент так и называется Type Library. Откроется редактор, и в него надо внести наши интерфейсы. При добавлении интерфейса он сразу является потомком IDispatch, как раз то, что требуется. Вот что у меня получилось в итоге:clip0037 Я объявил два интерфейса: IVector, обеспечивающий работу с массивом, и ISort, у которого есть один метод, принимающий на вход массив и сортирующий его. Кроме этого, есть перечисление CompareResult.

Отправить комментарий

Проверка
Антиспам проверка
Image CAPTCHA
...