До MEFа...
На моем текущем проекте есть модуль, который умеет анализировать файлы, полученные от пользователя, и вытягивать из них интересную для бизнеса информацию. В общих чертах структура модуля выглядит вот так:
ParseManager организует всю работу по анализу содержимого файлов. При этом он сам понятия не имеет, как именно извлекать данные. Зато по типу файла он может найти соответствующий парсер и делегировать ему эту задачу. Для каждого типа файлов реализован свой собственный парсер. Список доступных парсеров хранится в конфигурационном файле приложения. Это позволяет свободно изменять коллекцию парсеров.
Давайте посмотрим на пример, который я собрал по мотивам реального проекта. Интерфейс IParser описывыет контракт, который должны реализовывать все парсеры:
public interface IParser
{
IEnumerable<string> Parse();
}
Вот пример реализации парсера для текстовых файлов. Он не выполняет никакой полезной работы, а просто возвращает свое имя в качестве результата:
public class Parser : IParser
{
public IEnumerable<string> Parse()
{
return new List<string> { "TxtParser" };
}
}
Давайте теперь посмотрим, как организована инфраструктура подключения парсеров. В конфигурационном файле для этого отведена специальная секция:
<parseConfiguration>
<parsers>
<add extension="docx" type="DocxParser.Parser, DocxParser" />
<add extension="txt" type="TxtParser.Parser, TxtParser" />
</parsers>
</parseConfiguration>
Для того чтобы можно было работать с содержимым parseConfiguration секции, мы реализовали три класса:
- ParseSettings – описывающий саму секцию
- ParserDataCollection – описывающий коллекцию элементов нашей секции
- ParserData – описывающий отдельный элемент коллекции, т.е. отдельный парсер
Я не буду приводить здесь код этих классов, вы сможете скачать их вместе с остальными исходниками этого проекта отсюда.
И наконец, самый интересный момент. Давайте посмотрим, как же приложение находит нужный парсер и создает его экземпляр. Тут все достаточно просто:
- Загружаем из конфигурационного файла нужную нас секцию.
- Среди элементов этой секции ищем тот, у которого свойство Extension соответствует типу полученного от пользователя файла.
private static IEnumerable<string> Parse(string fileName)
{
IParser parser = null;
string fileExtension =
Path.GetExtension(fileName).Substring(1);
ParseSettings settings =
(ParseSettings)ConfigurationManager.GetSection(
ParseSettings.ParseSettingsSection);
ParserData parserSettings = (
from parserData in settings.Parsers.Cast<ParserData>()
where string.Equals(
parserData.Extension,
fileExtension,
StringComparison.OrdinalIgnoreCase)
select parserData
).FirstOrDefault();
Type parserType = Type.GetType(parserSettings.ParserType, true);
parser = Activator.CreateInstance(parserType) as IParser;
return parser.Parse();
}
Основным недостатком этого подхода, на мой взгляд, является необходимость написания большого объема вспомогательного кода. Если нам понадобится добавить в приложение какой-то другой вид плагинов, например модули для создания изображений (preview) для наших файлов, то нам придется добавлять в конфигурацию еще одну секцию и еще раз реализовывать три класса описывающие ее содержимое. Кроме того, если понадобится использовать наши парсеры в другом приложении, то мы не сможем отдать их просто так. В новом приложении тоже надо будет организовывать работу с конфигурацией.
Если вы дочитали до этого места, то наверняка уже не раз подумали: «Ну а где же собственно про MEF?» Не переживайте, сейчас все будет :)
Немного теории
Мне лениво придумывать свое определение для MEF, поэтому я просто переведу немного текста с официального сайта: «MEF (Managed Extensibility Framework) упрощает создание расширяемых приложений, предлагая функционал по обнаружению и подключению модулей.»
Основные понятия MEFа:
- Composable part — модуль, который предоставляет сервисы другим модулям или использует сервисы других модулей. Модули в MEF могут находиться как в отдельных сборках, так и в одной с основным приложением сборке.
- Export—Функциональность которую модуль предоставляет для использования другими модулями.
- Import—Функциональность, которую модуль хочет получить от других модулей.
- Contracts—Строковый идентификатор для экспорта и импорта. Экспортер указывает контракт для каждого экспорта, который он предоставляет. Импортер со своей стороны указывает контракт для каждого импорта, который он потребляет.
- Composition—Используя коллекцию модулей, MEF строит граф зависимостей между ними, пытаясь каждому импорту найти соответствующий экспорт.
- Catalog – содержит коллекцию доступных модулей
- Container – используя каталог, строит композицию модулей.
Если не вдаваться в детали, то можно очень просто описать как работает MEF: Сперва Catalog формирует коллекцию доступных модулей, затем Container пытается найти для каждого импорта, соответствующий экспорт.
MEF в деле...
Итак, давайте посмотрим, насколько использование MEF облегчит нам жизнь. Как вы уже, наверное, догадались, я не зря в начале статьи так долго рассматривал пример с парсерами. Сейчас я перепишу его, используя плюшки MEFа.
Интерфейс IParser оставляем без изменений:
public interface IParser
{
IEnumerable<string> Parse();
}
А вот парсеры получат новые атрибуты:
[Export(typeof(IParser))]
[PartMetadata("Extension", "txt")]
public class Parser : IParser
{
public IEnumerable<string> Parse()
{
return new List<string> { "TxtParser" };
}
}
Здесь атрибут Export означает, что мы хотим пометить класс Parser как доступный для инфраструктуры MEF при построении композиции. typeof(IParser) в экспорте означает, что контрактом (строковым идентификатором) для экспорта будет “PluginsViaMEF.Contracts.IParser”. Позже я покаже как мы сможем использовать этот экспорт.
Давайте теперь посмотрим на второй атрибут парсера – PartMetadata. Этот атрибут позволяет привязывать к модулям различную вспомогательную информацию. Он позволяет множественное использование и представляет собой словарь со строковым ключом. Позже вы увидите как можно из кода получить доступ к метаданным модуля.
Как вы видите, мы сохранили в метаданные парсера тип файлов, с которым этот парсер может работать. Т.е больше нет необходимости хранить эту информацию в файле конфигурации. Кроме того список доступных парсеров мы теперь будем получать с помощью MEF. Получается что нам боьше нечего хранить в конфигурации. Мы уберем из нее секцию связанную с парсерами. И заодно удалим из проекта классы ParseSettings, ParserDataCollection и ParserData. Они нам больше не нужны.
Давайте теперь посмотрим что изменилось в основном приложении и как оно теперь будет находить и подключать парсеры. Во-первых мы добавим в класс Programm поле _parser:
[Import]
public IParser _parser;
Обратите внимание на атрибут Import. Мы не указываем здесь явно контракт. По умолчанию MEF использует как контракт тип объекта помеченного атрибутом. Т.е. в нашем случае это будет “PluginsViaMEF.Contracts.IParser”. Вы помните, что именно с таким контрактом мы экспортировали наши парсеры. Теперь, при построении композиции, MEF проинициализирует поле _parser экземпляром одного из парсеров. Наша задача помочь ему выбрать конкретный парсер. Т.к. если MEF обнаружит несколько экспортов соответствующих одному импорту, то он выкинет исключение и перестанет строить композицию.
private void Compose(string fileExtension)
{
var container = new CompositionContainer();
var catalog = new DirectoryCatalog(@".\");
CompositionBatch batch = new CompositionBatch();
batch.AddPart(this);
batch.AddPart(
catalog
.Parts
.Where(
p => p.Metadata.ContainsKey("Extension") &&
string.Equals(
p.Metadata["Extension"].ToString(),
fileExtension,
StringComparison.OrdinalIgnoreCase)
)
.First().CreatePart());
container.Compose(batch);
}
Итак, для того чтобы получить список доступных парсеров, мы создаем DirectoryCatalog и скармливаем ему папку в которой должны лежать наши парсеры. Далее мы используем мета-данные парсеров, чтобы получить только один парсер, который соответствует типу полученного от пользователя файла. И наконец, контейнеру для построения композиции мы даем только основную сборку программы и найденный парсер. Контейнер создаст экземпляр парсера и проинициализирует им поле _pаrser в главном модуле.
На этом у меня все. Проекты с исходным кодом можно скачать здесь.
Ссылки:
- MEF community site on Codeplex
- The Managed Extensibility Framework by Ayende Rahien
No comments:
Post a Comment