Немного о плагинахDelphi , Файловая система , DLL и PlugInsНемного о плагинах
Введение Проблема декомпозиции приложения рано или поздно возникает в любом серьезном проекте. Цели декомпозиции могут быть различны, но можно выделить наиболее часто встречающиеся: Облегчение сопровождения. Приложение разбивается на функциональные модули так, чтобы можно было без опаски заменить "ошибочный" модуль на (якобы) "исправленный" 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. Вот текст этой процедуры
Теперь сделаем собственно пакет (10) . В него поместим две формы, одну из которых сделаем дочерней (MDIChild), а на другую положим две кнопки (Ok и Cancel). Далее организуем в главной форме загрузку пакета и вызов из него форм. Для этого на OnShow делаем вызов LoadPluginPackage и добавляем actions в ActionList: Для дочерней формы
Для диалога
Плюс ко всему добавляем обработчик OnUpdate на все action'ы для обеспеченя корректного вызова
Полный исходный код находится в архиве (каталог Step1) Часть 2. Доступ к объектам пакета. Попытаемся наладить связь между главной формой и пакетом. То есть, мы ставим себе задачу вызова некоторой (или некоторых) функции/процедур формы из пакета при условии того, что мы не знаем действительный тип этой формы и всего набора поддерживаемых ею фун кций и процедур. Для осуществления этого воспользуемся технологией COM. Точнее той ее части, которая поддерживается Delphi на уровне языка. Для того, чтобы новичкам было все ясно, следует немного углубиться в понятие интерфейса. Я полагаю, что вы знакомы с понятием виртуальной таблицы методов (VMT). Именно она является источником и тремя составными частями ООП (11). Для поддержки COM в Delphi был введен новый, особый тип interface, который позволяет "поименовать" куски виртуальной таблицы методов. Способ этого именования достаточно уникален - 16-байтовое число (12), которое присваивается каждому такому куску. Есть мнение, что оно статистическ и уникально (13). Синтаксис данного типа следующий
Помимо прочего компоненты Delphi содержат метод, позволяющий получать этот кусок виртуальной таблицы. Причем этих методов два. Первый имеет название QueryInterface. Для любителей COM он является привычным, так как используется в оном вдоль и поперек. Второй называется GetInterface. Разница этими методами в том, что у QueryInterface нужно проверять результат на S_OK, а у GetInterface на Boolean (14) . Теперь давайте прикрутим к нашей системе интерфейсы и покажем, как с ними работать. Для этого создаем новый модуль (он достаточно короткий, и я здесь привожу его полностью)
Что мы тут сделали? Мы сказали, что TfrmChild является наследником TForm, но помимо методов TForm VMT класа TfrmChild содержит еще два цельных куска, один из которых идентичен VMT IMyInitialize, а второй VMT IMyHello. 2. для диалоговой формы:
Реализация этих методов проста, ее можно смотреть в архиве (Step2). Соответственно (для вызова этих методов), немного корректируем главную форму…
Теперь что мы имеем. Во-первых, мы не знаем действительный тип как дочернего, так и диалогового окон. Но между тем вызываем функции, входящие в его VMT. Во-вторых, мы запросто можем поменять наш пакет на другой. Имена классов форм пакета особого значения не имеют - их можно сохранять в файле настроек или реестре и подгружать при инициализации основного приложения. Единственное, что необходимо неукоснитель но соблюдать - дочернее и диалоговое окно ДОЛЖНЫ поддерживать необходимые интерфейсы. В третьих, мы можем КАК УГОДНО изменять формы пакета (включая изменения самой виртуальной таблицы методов, естественно, не затрагивая описания интерфейсов) - общая система приложение-пакет останутся в рабочем состоянии. Часть 3. Взаимодействие пакета с приложением При разработке проекта довольно часто встречается ситуация, когда одна из форм (модулей данных, компонент или, наконец, просто объектов) обращается к методам второй формы, а та, в свою очередь, нуждается в вызове методов (или в доступе к свойствам) первой . Иногда эта ситуация вообще трудно разрешима (если оказываются необходимыми перекрестные ссылки в интерфейсных частях модулей — это недопустимо правилами языка)(15) . Очень часто взаимодействие модулей проекта, форм и т.д. оказывается до такой степени пе репутанным, что разобраться в этих хитросплетениях бывает тяжело (особенно если этот проект передается для дальнейшего сопровождения и доработки другому программисту). Часть таких проблем вполне может снять использование интерфейсов. Действительно, в пред ыдущей части для использования методов форм из пакета нам не потребовалось подключать модули, содержащие их реализацию. Что помешает использовать ту же технологию и в обратном направлении?
Если задуматься над этой процедурой, то станет ясно, что нам не важен тип активной дочерней формы и местоположение реализации этого типа (в главном приложении находится ее модуль, в том же пакете, что и TfrmDialogFrom, или где-нибудь еще). Мы просто обнар ужили, что есть какая-то активная форма, спросили ее на предмет поддержки конкретного интерфейса и вызвали его метод. Часть 4. Некоторые нюансы В связи с тем, что мы "разбиваем" виртуальную таблицу методов наших форм на "куски"-интерфейсы, возможны ситуации, когда несколько интерфейсов будут содержать методы с одинаковым названием. Способ обработки таких случаев известен программистам, работавшим с COM. Для тех, кому он неизвестен, я сейчас его продемонстрирую. Добавим еще один интерфейс в модуль CommonInterfaces и назавем его ICallbackInterface2. В интерфейсе опишем процедуру с названием, пересекающимся с ICallbackInterface:
Теперь после компиляции что мы получим? Вызов 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) .
Остается заметить, что этот модуль желательно оформить в виде отдельного пакета, установить его в системе и компилировать с ним как основное приложение, так и пакеты-плугины. Теперь, после всего выше сказанного, нетрудно осуществить непосредственную агрегацию. Первое место, где она с успехом может быть применена - это приложения с использованием БД. Обычно в этом случае основное приложение имеет (помимо главной формы) один или несколько модулей данных (наследников 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 этого делать не нужно. Наверное, существует что-то еще (что определиться опытным путем в дальнейшем или умные люди подскажут … This text is a detailed description of how to use interfaces in Delphi programming. The author provides several examples and explanations to help readers understand the concept of interfaces, their benefits, and how to implement them. Here are some key p Комментарии и вопросыПолучайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта. :: Главная :: DLL и PlugIns ::
|
|||||||||||||||||
©KANSoftWare (разработка программного обеспечения, создание программ, создание интерактивных сайтов), 2007 |