Современные тенденции в разработке информационных систем требуют от проектировщиков закладывать в архитектуру систем возможность динамического расширения их функционала. И не смотря на существование огромного количества наработок в этом направлении, единого решения структуры модульного приложения нет. Использование же готового решения не всегда возможно в силу специфики языка программирования или разрабатываемой системы. Так же, готовые решения модульных систем не всегда доступны для изучения, а иногда излишне сложны.
Модули в разных системах, зачастую, имеют разные границы функциональности. В системе могут быть выделены некоторые, строго определенные точки расширения – некоторый функционал, дополняемый сторонними разработчиками. Или система может представлять собой лишь механизм управления модулями, а весь ее функционал реализуется отдельными модулями.
Вперые опубликовано на habrahabr.ru
Этапы проектирования модульных приложений
Для разработки модульного приложения, прежде всего, необходимо выделить тот функционал, который должен расширяться с помощью модулей.
Далее разрабатываются интерфейсы, с помощью которых система будет обращаться к сторонним реализациям за этим функционалом.
Самым тонким моментом становится вопрос о том, как динамически добавлять реализации интерфейсов.
Reflection
В .Net Framework присутствует мощная технология reflection. Reflection позволяет программе отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Применительно к нашей задаче, reflection позволяет загружать библиотеки в память и проверять все реализованные в ней классы на предмет реализации необходимых интерфейсов.
Т.е., можно добавлять библиотеки (с реализацией выделенных интерфейсов), представляющие собой модули, в специальную директорию и с помощью технологии reflection находить и инстанцировать необходимые классы.Это наиболее популярное решение, которое часто встречается на просторах интернета. Но у подобного подхода есть существенные недостатки, связанные с высокой ресурсоемкостью.
Загрузка сборки в память и перебор всех доступных в ней классов, в поисках реализаций интерфейсов, требует большого количества оперативной памяти, и ощутимого времени на выполнение перебора. Все усложняется в том случае, если реализация метода выходит за рамки одной библиотеки и имеет зависимости от сторонних библиотек. Тогда под перебор попадают сборки, вообще не содержащие реализаций необходимых интерфейсов и процессорное время, затрачиваемое на их исследование, тратится впустую.
Структура модуля
Очевидно, чтобы исключить лишние библиотеки из перебора, необходима дополнительная информация о модуле. Подобным источником информации может выступить текстовый файл, сопровождающий библиотеки модуля и предоставляющий информацию о них. Таким образом, перед нами встает задача разработки требований к структуре модуля, одним из пунктов которого можно предложить требование наличия файла, указывающего на главную библиотеку с реализацией интерфейсов и содержащего перечень всех необходимых зависимостей.
Простейшим решением устройства модуля может быть следующее:
1. Модуль представляет собой архив всех необходимых библиотек. В качестве алгоритма сжатия может выступать zip. Причем, как такового сжатия не требуется (бинарные библиотеки плохо поддаются архивированию), необходимо просто объединить все составляющие модуля в один файл.
2. Кроме библиотек, модуль должен содержать текстовый файл (называемый дескриптором), содержащий информацию о главной библиотеке, зависимостях, и, для повышения быстродействия путем исключения перебора, название реализуемого интерфейса вместе с полным именем класса его реализующего.
Для реализации дескриптора плагина представляется удобным воспользоваться XML.
Простейшая структура такого документа может иметь следующий вид:
<?xml version="1.0"?>
<plugin>
<type>Имя интерфейса</type>
<name>Имя модуля</name>
<description>Описание модуля.</description>
<version>Номер версии модуля</version>
<class>
Полное имя класса, реализующего указанный интерфейс
</class>
<assembly>Главная библиотека</assembly>
<dependences>
<dependence>Дополнительная библиотека</dependence>
</dependences>
</plugin>
Добавление и удаление модулей
Добавление нового модуля в систему может происходить в следующей последовательности:
1. Системе передается полный путь файла с добавляемым модулем.
2. Добавляемый модуль проверяется на соответствие своему дескриптору: проверяется наличие всех указанных библиотек, наличие главного класса и реализация им указанного интерфейса.
3. В директории системы, отведенной под хранение модулей, создается новая поддиректория для добавляемого модуля. Все библиотеки модуля копируются в эту директорию.
4. Вычисляется уникальный идентификатор* модуля (как вариант, можно взять хеш от имени и версии модуля).
5. Вся информация из дескриптора модуля и вычисленный идентификатор записываются в системный реестр модулей (xml-файл, хранящий информацию об установленных в системе модулях).
________________________________________
*этот идентификатор вводится на случай, если возникнет необходимость сохранения информации об использовании модуля в прошлой сессии работы в системе
В простейшем случае реестр модулей может представлять собой xml-файл со схожей с приведенной выше структурой, с той лишь разницей, что записей о модулях в ней будет много:
<?xml version="1.0"?>
<plugins>
<plugin>
<id>534523</id>
<type>Имя интерфейса</type>
<name>Имя модуля 1</name>
<description>Описание модуля 1</description>
<version>Номер версии модуля 1</version>
<class>
Полное имя класса, реализующего указанный интерфейс
</class>
<assembly>Главная библиотека</assembly>
<dependences>
<dependence>Дополнительная библиотека</dependence>
</dependences>
</plugin>
<plugin>>
<id>79568</id>
<type>Имя интерфейса</type>
<name>Имя модуля 2</name>
<description>Описание модуля 2</description>
<version>Номер версии модуля 2</version>
<class>
Полное имя класса, реализующего указанный интерфейс
</class>
<assembly>Главная библиотека</assembly>
<dependences>
<dependence>Дополнительная библиотека</dependence>
</dependences>
</plugin>
. . . .
</plugins>
Последовательность действий для удаления модуля:
1. Удаление директории модуля**.
2. Удаление информации о модуле из реестра.
________________________________________
**здесь следует заметить следующий нюанс: инициирование удаления модуля пользователем может быть выполнено при активном использовании этого модуля системой, следствием чего может быть блокировка библиотек модуля. К этой проблеме я еще вернусь ниже.
Структура классов
PluginDescriptor — предоставляет информацию о модуле, достаточную для его инстанцирования.
PluginRegister — выполняет операции чтения и записи в реестр модулей. Возвращает дескриптор модуля, соответствующего указанному идентификатору. Может вернуть полный список дескрипторов всех доступных модулей.
Plugin — предоставляет информацию о модуле, основываясь на соответствующей информации в дескрипторе.Содержит ссылку на объект, представляющий собой реализацию модуля. Возможно, существование этого класса может показаться избыточным. Но его ценность проявляется в том случае, если решение о целесообразности использования модуля пользователь принимает на основании его (модуля) описании. В таком случае, мы можем предоставить всю необходимую пользователю информацию не загружая библиотек модуля.
PluginManager — выполняет добавление и удаление модулей в системе. Осуществляет контроль целостности модуля при его добавлении. Реализуется в соответствии с шаблоном «одиночка», для унификации способов получения модулей.
PluginLoader — выполняет инстанцирование модуля. Необходимость введения этого класса вызвана спецификой самого процесса инстанцирования, речь о которой пойдет ниже (так же можно прочесть эту статью).
Нюансы динамического подключения модулей, специфичные для .Net Framework
У динамической загрузки сборок в .Net есть особенность — сборки загружаются в так называемые AppDomain — домены приложения, являющиеся изолированной средой, в которой выполняются приложения. Нюанс в том, что выгрузка сборки отдельно невозможна. Ее возможно произвести только выгрузив весь домен целиком. Таким образом, после инициализации модуля все используемые им библиотеки блокируются для удаления из файловой системы, и их выгрузка из памяти становится невозможной.
Для разрешения проблемы с блокированием файлов библиотек существует решение, называемое теневое копирование. Его суть заключается в копировании файла библиотеки и загрузки в память уже ее копии, оригинал при этом остается доступен для удаления.
Решение проблемы, связанной с выгрузкой ранее загруженного модуля из памяти, заключается в создании для модуля отдельного домена. Помимо решения проблемы с выгрузкой модуля из памяти, это решение открывает возможность ограничения прав исполняемого кода. Например, модулю можно запретить чтение и запись в любой директории отличной от той, в которой он располагается. Но у подобного решения есть и обратная сторона, требующая сериализации классов, реализующих интерфейсы модулей.
Для наглядности приведу код класса PluginLoader и часть кода класса Plugin (в приведенном коде не учитывается наличие зависимостей загружаемого модуля):
public class BasePlugin
{
...
/// <summary>
/// Возвращает реализацию инкапсулируемого расширения.
/// </summary>
/// <remarks>
/// Инстанцирование расширения происходит при первом обращении.
/// </remarks>
public Object Instance {
get {
if (instance == null) {
instance = CreateInstance();
}
return instance;
}
}
/// <summary>
/// Создает домен для загружаемой реализации модуля
/// и загружает ее в него.
/// </summary>
/// <returns>
/// В случае успеха загруженное расширение, иначе null.
/// </returns>
private Object CreateInstance()
{
Descriptor d = this.Descriptor;
/* Настраиваем домен */
AppDomainSetup setup = new AppDomainSetup();
setup.ShadowCopyFiles = "true"; // включаем теневое копирование
//TODO: Задать настройки безопасности для плагина
/* Создаем домен для плагина */
AppDomain domain = AppDomain.CreateDomain(d.ToString(), null, setup);
/* Создаем загрузчик плагина */
PluginLoader loader =
(PluginLoader)domain.CreateInstanceFromAndUnwrap(
typeof(PluginLoader).Assembly.Location,
typeof(PluginLoader).FullName);
/* Создаем экземпляр плагина */
Object obj = null;
try {
obj = loader.CreateInstance(d, PluginManager
.GetPluginDirectory(this));
} catch (Exception e) {
AppDomain.Unload(domain);
throw new PluginLoadException(d, "", e);
}
return obj;
}
}
[Serializable]
public class PluginLoader
{
/// <summary>
/// Загружает сборку модуля и возвращает реализацию модуля.
/// </summary>
/// <param name="d">Дескриптор загружаемого модуля.</param>
/// <param name="pluginDirectory">
/// Полный путь к директории загружаемого модуля.
/// </param>
/// <returns>
/// Реализацию модуля, соответствующего переданному дескриптору.
/// </returns>
public Object CreateInstance(Descriptor d, String pluginDirectory)
{
String assemblyFile = Path.Combine(pluginDirectory, d.AssemblyName);
/* Пытаемся загрузить сборку. */
Assembly assembly = null;
try {
AssemblyName asname =
AssemblyName.GetAssemblyName(assemblyFile);
assembly = AppDomain.CurrentDomain.Load(asname);
} catch (Exception e) {
throw new AssemblyLoadException(assemblyFile,
"Ошибка загрузки сборки.", e);
}
/* Пробуем получить объект класса, реализующего модуль. */
Object obj = null;
try {
obj = assembly.CreateInstance(d.ClassName);
} catch (Exception e) {
throw new ClassLoadException(d.ClassName, assembly.FullName,
"Ошибка при получении экземпляра класса.", e);
}
return obj;
}
}
Заключение
Чтобы не бросать статью на голом коде, подведу итог всего выше изложенного. Все приведенные соображения были испытаны мной всего в одном проекте, поэтому не исключено, что в Вашем опыте следования этой статье возникнут незамеченные мной трудности.
Из заведомо нерешенных проблем, могу привести следующее: возможны избыточные расходы ресурсов в том случае, если модули имеют в зависимостях одинаковые библиотеки.
1 комментарий:
Теперь нашел нужную информацию.Буду разбираться и использовать.
Отправить комментарий