Managed Extensibility Framework (MEF) предназначена для того, чтобы разработчики, использующие Microsoft .NET Framework, могли легко создавать свободно связанные приложения. Основное внимание в MEF первой версии было уделено расширяемости, чтобы разработчик приложения мог предоставлять определенные точки расширения сторонним разработчикам, а те могли создавать надстройки или расширения для этого приложения или его компонентов. Модель плагинов в Visual Studio для расширения самой этой среды — отличный пример использования такого варианта; детали см. в MSDN Library на странице «Developing Visual Studio Extensions» (bit.ly/IkJQsZ). Этот способ предоставления точек расширения и определения плагинов использует как раз то, что называют моделью атрибутивного программирования, при которой разработчик может дополнять свойства, классы и даже методы атрибутами для уведомления о том, что либо требуется зависимость от специфического типа, либо возможность удовлетворить такую зависимость.
Несмотря на тот факт, что атрибуты очень полезны в тех сценариях расширения, где система типов является открытой, это все же перебор для закрытых систем типов, известных в период компиляции. Некоторые из фундаментальных проблем с моделью атрибутивного программирования перечислены ниже.
- Конфигурация для многих сходных фрагментов включает уйму ненужных повторений; это нарушает принцип Don’t Repeat Yourself (DRY) и на практике может вести к ошибкам; кроме того, файлы исходного кода труднее читать.
- Создание расширения или фрагмента в .NET Framework 4 означает зависимость от сборок MEF, которые привязывают разработчика к определенной инфраструктуре встраивания зависимостей (dependency injection, DI).
- Фрагменты, которые создавались без учета MEF, требуют добавления атрибутов для корректной идентификации в приложениях. Это может послужить серьезным барьером для их внедрения.
.NET Framework 4.5 позволяет централизовать конфигурацию, чтобы вы могли написать набор правил, определяющих, как создаются и составляются точки расширения и компоненты. Это достигается использованием нового класса RegistrationBuilder (bit.ly/HsCLrG), который находится в пространстве имен System.ComponentModel.Composition.Registration. В этой статье я сначала рассмотрю некоторые причины для использования такой системы, как MEF. Если вы ветеран в работе с MEF, то вполне можете пропустить эту часть. Затем я примерю на себя роль разработчика, которому выдали набор требований и который должен создать простое консольное приложение, используя MEF-модель атрибутивного программирования. Далее я преобразую это приложение в модель на основе соглашений, продемонстрировав, как реализовать некоторые типичные сценарии с применением RegistrationBuilder. В заключение мы обсудим, как конфигурация, управляемая соглашением, интегрируется в модели приложений и как она делает использование MEF и принципов DI тривиальной задачей.
Обзор
По мере роста проектов в размерах и масштабах на первый план выходят проблемы сопровождения, расширяемости и возможности тестирования. Когда эти проекты взрослеют, может потребоваться замена или переработка каких-либо компонентов. А когда область применения проектов расширяется, требования к ним зачастую меняются или дополняются. Возможность добавлять функциональность в крупный проект без особых усилий крайне важна для эволюции этого продукта. Более того, сейчас, когда изменения становятся нормой в большинстве жизненных циклов ПО, чрезвычайно важна и возможность быстрого тестирования компонентов, являющихся частью программного продукта, независимо от других компонентов — особенно в средах, где зависимые компоненты разрабатываются параллельно.
Из-за этих факторов концепция DI стала популярной в проектах разработки крупномасштабного ПО. Суть идеи DI — разработка компонентов, оповещающих о необходимых зависимостях без создания их экземпляров и о зависимостях, которым они удовлетворяют, а уж инфраструктура DI берет на себя задачу распознавания всего этого и «встраивает» правильные экземпляры зависимостей в компонент. Статья «Dependency Injection», опубликованная в номере «MSDN Magazine» за сентябрь 2005 г. (msdn.microsoft.com/magazine/cc163739), — отличный материал, если вам нужен более подробный обзор на эту тему.
Сценарий
Теперь перейдем к сценарию, который я описывал ранее: я разработчик, изучающий выданную мне спецификацию. На высоком уровне цель решения, которое мне предстоит реализовать, — предоставлять прогноз погоды пользователю на основе его ZIP-кода. Ниже перечислены необходимые шаги.
- Приложение запрашивает ZIP-код от пользователя.
- Пользователь вводит допустимый ZIP-код.
- Приложение взаимодействует с провайдером метеорологического сервиса в Интернете, чтобы получить прогноз.
- Приложение предоставляет пользователю эту информацию в отформатированном виде.
С точки зрения требований, очевидно, что на этом этапе есть некоторые неизвестные или аспекты, которые потенциально изменятся позднее в цикле разработки. Например, я пока не знаю, какой провайдер метеорологического сервиса я буду использовать или каким способом я буду получать данные от провайдера. Поэтому, приступая к проектированию этого приложения, я разобью продукт на несколько дискретных функциональных блоков: WeatherServiceView, IWeatherServiceProvider и IDataSource. Код каждого из этих классов показан соответственно на рис. 1, 2 и 3.
Рис. 1. WeatherServiceView — класс, отображающий результаты
[Export]
public class WeatherServiceView
{
private IWeatherServiceProvider _provider;
[ImportingConstructor]
public WeatherServiceView(IWeatherServiceProvider providers)
{
_providers = providers;
}
public void GetWeatherForecast(int zipCode)
{
var result=_provider.GetWeatherForecast(zipCode);
// Какая-то логика отображения
}
}
Рис. 2. Сервис разбора данных IWeatherServiceProvider (WeatherUnderground)
[Export(typeof(IWeatherServiceProvider))]
class WeatherUndergroundServiceProvider:IWeatherServiceProvider
{
private IDataSource _source;
[ImportingConstructor]
public WeatherUndergroundServiceProvider(IDataSource source)
{
_source = source;
}
public string GetWeatherForecast(int zipCode)
{
string val = _source.GetData(GetResourcePath(zipCode));
// Какая-то логика разбора
return result;
}
private string GetResourcePath(int zipCode)
{
// Какая-то логика получения адреса ресурса
}
}
Рис. 3. IDataSource (WeatherFileSource)
[Export(typeof(IDataSource))]
class WeatherFileSource :IDataSource
{
public string GetData(string resourceLocation)
{
Console.WriteLine("Opened ----> File Weather Source ");
StringBuilder builder = new StringBuilder();
using (var reader = new StreamReader(resourceLocation))
{
string line;
while((line=reader.ReadLine())!=null)
{
builder.Append(line);
}
}
return builder.ToString();
}
}
Наконец, чтобы создать иерархию фрагментов (parts), мне нужно использовать Catalog для поиска всех фрагментов в приложении, а потом задействовать CompositionContainer для получения экземпляра WeatherServiceView, с которым я смогу оперировать, например так:
class Program
{
static void Main(string[] args)
{
AssemblyCatalog cat =
new AssemblyCatalog(typeof(Program).Assembly);
CompositionContainer container =
new CompositionContainer(cat);
WeatherServiceView forecaster =
container.GetExportedValue<WeatherServiceView>();
// Принимаем ZIP-код и вызываем средство просмотра
forecaster.GetWeatherForecast(zipCode);
}
}
Весь код, представленный мной на данный момент, использует базовую семантику MEF; если вам не совсем ясно, как работает этот код, пожалуйста, загляните в MSDN Library на страницу «Managed Extensibility Framework Overview» (bit.ly/JLJl8y), где подробно описывается MEF-модель атрибутивного программирования.
Конфигурация, управляемая соглашением
Теперь, когда у меня есть работающая атрибутивная версия моего кода, я хочу продемонстрировать, как преобразовать эти части кода в модель на основе соглашения с применением RegistrationBuilder. Начнем с удаления всех классов, к которым были добавлены MEF-атрибуты. В качестве примера рассмотрим фрагмент кода на рис. 4, написанного на основе сервиса разбора данных WeatherUnderground с рис. 2.
Рис. 4. Класс разбора данных WeatherUnderground, преобразованный в простой C#-класс
class WeatherUndergroundServiceProvider:IWeatherServiceProvider
{
private IDataSource _source;
public WeatherUndergroundServiceProvider(IDataSource source)
{
_source = source;
}
public string GetWeatherForecast(int zipCode)
{
string val = _source.GetData(GetResourcePath(zipCode));
// Какая-то логика разбора
return result;
}
...
}
Код на рис. 1 и 3 будет изменен так же, как и на рис. 4.
Далее с помощью RegistrationBuilder я определяю некоторые соглашения, чтобы выразить то, что мы задавали через атрибуты. На рис. 5 показан соответствующий код.
Рис. 5. Задание соглашений
RegistrationBuilder builder = new RegistrationBuilder();
builder.ForType<WeatherServiceView>()
.Export()
.SelectConstructor(cinfos => cinfos[0]);
builder.ForTypesDerivedFrom<IWeatherServiceProvider>()
.Export<IWeatherServiceProvider>()
.SelectConstructor(cinfo => cinfo[0]);
builder.ForTypesDerivedFrom<IDataSource>()
.Export<IDataSource>();
Каждое объявление правила состоит из двух частей. Одна часть идентифицирует класс или набор классов, с которыми нужно оперировать, а другая — атрибуты, метаданные и политики совместного использования, применяемые к выбранным классам, свойствам классов или конструкторам классов. Таким образом, вы видите, что в строках 2, 5 и 8 начинаются три определенных мной правила и первая часть каждого правила идентифицирует тип, к которому будет применяться остальная часть правила. Например, в строке 5 я хочу применить соглашение ко всем типам, производным от IWeatherServiceProvider.
Теперь посмотрим на правила и вновь сопоставим их с исходным атрибутивным кодом на рис. 1, 2 и 3. WeatherFileSource (рис. 3) просто экспортировался как IDataSource. На рис. 5 правило в строках 8 и 9 указывает выбрать все типы, производные от IDataSource, и экспортировать их как контракты IDataSource. На рис. 2 видно, что код экспортирует тип IWeatherServiceProvider и требует импорта IDataSource в своем конструкторе, который был дополнен атрибутом ImportingConstructor. Соответствующее правило для этого на рис. 5 задается в строках 5, 6 и 7. Здесь добавлен метод SelectConstructor, принимающий Func<ConstructorInfo[], ConstructorInfo>. Это дает мне возможность указать конструктор. Вы можете определить соглашение, что, скажем, конструктор с самым малым или самым большим числом аргументов всегда будет ImportingConstructor. В моем примере, поскольку у меня только один конструктор, я могу использовать тривиальный случай выбора первого и единственного конструктора. Для кода на рис. 1 правило на рис. 5 определено в строках 2, 3 и 4 и аналогично тому правилу, которое мы только что обсуждали.
Задав правила, мне нужно применить их к типам, присутствующим в приложении. Для этого во всех каталогах теперь имеется перегруженная версия, которая принимает RegistrationBuilder в качестве параметра. Поэтому вы должны модифицировать предыдущий код CompositionContainer, как показано на рис. 6.
Рис. 6. Использование соглашений
class Program
{
static void Main(string[] args)
{
// Здесь размещаем код для создания RegistrationBuilder
AssemblyCatalog cat =
new AssemblyCatalog(typeof(Program).Assembly,builder);
CompositionContainer container = new CompositionContainer(cat);
WeatherServiceView forecaster =
container.GetExportedValue<WeatherServiceView>();
// Принимаем ZIP-код и вызываем средство просмотра
forecaster.GetWeatherForecast(zipCode);
}
}
Наборы
Теперь все готово, и мое простое MEF-приложение работает без атрибутов. Если бы жизнь была такой простой! Теперь мне сообщают, что мое приложение должно поддерживать более одного сервиса прогнозов погоды и что требуется показывать прогнозы ото всех сервисов. К счастью, поскольку я использовал MEF, у меня нет причин впадать в панику. Это просто сценарий с несколькими реализаторами интерфейса, и мне нужно выполнять итерации по ним. В моем примере теперь более одной реализации IWeatherServiceProvider, и я хочу отображать результаты ото всех этих сервисов. Давайте рассмотрим необходимые изменения, показанные на рис. 7.
Рис. 7. Поддержка нескольких IWeatherServiceProvider
public class WeatherServiceView
{
private IEnumerable<IWeatherServiceProvider> _providers;
public WeatherServiceView(IEnumerable< IWeatherServiceProvider> providers)
{
_providers = providers;
}
public void GetWeatherForecast(int zipCode)
{
foreach (var _provider in _providers)
{
Console.WriteLine("Weather Forecast");
Console.WriteLine(_provider.GetWeatherForecast(zipCode));
}
}
}
Вот и все! Я изменил класс WeatherServiceView так, чтобы принимать одну или более реализаций IWeatherServiceProvider, а в разделе логики я прохожу этот набор в цикле. Соглашения, заданные ранее, теперь будут захватывать все реализации IWeatherServiceProvider и экспортировать их. Однако в моем соглашении как будто чего-то не хватает: в некий момент мне пришлось добавить атрибут ImportMany или эквивалентное соглашение, когда я конфигурировал WeatherServiceView. Тут есть немного магии RegistrationBuilder, который определяет, что, если ваш параметр имеет IEnumerable<T>, то он должен быть ImportMany; при этом от вас не требуется явным образом указывать это. Так что использование MEF значительно упрощает расширение моего приложения, и благодаря RegistrationBuilder (при условии, что новая версия реализовала IWeaterServiceProvider) мне не пришлось делать ничего, чтобы расширение начало работать с моим приложением. Великолепно!
Метаданные
Другая по-настоящему полезная функциональность MEF — возможность добавления метаданных в фрагменты. Предположим, что в примере, который мы рассматривали, значение, возвращаемое методом GetResourcePath (рис. 2), управляется конкретным типом IDataSource и используемым IWeatherServiceProvider. Поэтому я определяю соглашение об именовании, указывающее, что имя ресурса будет формироваться из имен провайдера метеорологического сервиса и источника данных, разделяемых знаком подчеркивания. При таком соглашении провайдер сервисов Weather Underground с источником данных Web получит имя WeatherUnderground_Web_ResourceString. Соответствующий код приведен на рис. 8.
Рис. 8. Определение описания ресурса
public class ResourceInformation
{
public string Google_Web_ResourceString
{
get { return "http://www.google.com/ig/api?weather="; }
}
public string Google_File_ResourceString
{
get { return @".\GoogleWeather.txt"; }
}
public string WeatherUnderground_Web_ResourceString
{
get { return
"http://api.wunderground.com/api/96863c0d67baa805/conditions/q/"; }
}
}
Используя это соглашение, я могу теперь создать свойство в провайдерах метеорологических сервисов WeatherUnderground и Google, которое будет импортировать все эти строки ресурсов, и на основе их текущих конфигураций выбирать подходящий провайдер. Сначала посмотрим, как написать правило RegistrationBuilder для конфигурирования ResourceInformation в качестве Export (рис. 9).
Рис. 9. Правило для экспорта свойств и добавления метаданных
builder.ForType<ResourceInformation>()
.ExportProperties(pinfo =>
pinfo.Name.Contains("ResourceString"),
(pinfo, eb) =>
{
eb.AsContractName("ResourceInfo");
string[] arr = pinfo.Name.Split(new char[] { '_' },
StringSplitOptions.RemoveEmptyEntries);
eb.AddMetadata("ResourceAffiliation", arr[0]);
eb.AddMetadata("ResourceLocation", arr[1]);
});
Строка 1 просто идентифицирует класс. В строке 2 определяется предикат, который выбирает все свойства этого класса, содержащие ResourceString, как диктует мое соглашение. Последний аргумент в ExportProperties — Action<PropertyInfo,ExportBuilder>, где я указываю, что мне нужно экспортировать все свойства, совпадающие с предикатом, определенным в строке 2 как именованный контракт ResourceInfo, и что я хочу добавлять метаданные в зависимости от результатов разбора имени этого свойства, используя ключи ResourceAffiliation и ResourceLocation. На стороне потребителя теперь нужно добавить свойство во все реализации IWeatherServiceProvider:
public IEnumerable<Lazy<string, IServiceDescription>> WeatherDataSources { get; set; }
А затем добавить следующий интерфейс для использования строго типизированных метаданных:
public interface IServiceDescription
{
string ResourceAffiliation { get; }
string ResourceLocation { get; }
}
Чтобы узнать больше об обычных и строго типизированных метаданных, прочитайте полезное учебное пособие по ссылке bit.ly/HAOwwW.
Теперь добавим правило в RegistrationBuilder для импорта всех фрагментов, имеющих контракт с именем ResourceInfo. Для этого я возьму существующее правило с рис. 5 (строки 5–7) и добавлю следующий блок:
builder.ForTypesDerivedFrom<IWeatherServiceProvider>()
.Export<IWeatherServiceProvider>()
.SelectConstructor(cinfo => cinfo[0]);
.ImportProperties<string>(pinfo => true,
(pinfo, ib) =>
ib.AsContractName("ResourceInfo"))
Строки 8–9 теперь указывают, что во всех типах, производных от IWeatherServiceProvider, должен быть Import, применяемый ко всем свойствам строкового типа, и импорт следует выполнять согласно контракту с именем ResourceInfo. При выполнении этого правила ранее добавленное свойство становится Import для всех контрактов с именем ResourceInfo. После этого я могу запросить перечисление, чтобы отфильтровать правильную строку ресурса на основе метаданных.
Конец атрибутов?
Если вы внимательно изучали обсуждавшиеся примеры, то, возможно, вам показалось, что атрибуты больше не нужны. Все, что вы могли бы сделать с помощью модели атрибутивного программирования, теперь можно делать, используя модель на основе соглашений. Я упомянул некоторые распространенные сценарии применения, где может помочь RegistrationBuilder, а более подробную информацию по RegistrationBuilder вы получите из отличной статьи Николаса Блюмхардта (Nicholas Blumhardt) по ссылке bit.ly/tVQA1J. Однако атрибуты все еще могут играть важную роль в мире MEF, управляемом соглашениями. Одна из значимых проблем с соглашениями в том, что они хороши только в том случае, если их соблюдают. Как только появляется исключение из правил, издержки сопровождения соглашений могут оказаться запретительно высокими. И здесь очень полезны атрибуты, которые позволяют переопределять соглашения. Допустим, что в класс ResourceInformation был добавлен некий новый ресурс, но его имя не соответствует соглашению, как показано на рис. 10.
Рис. 10. Переопределение соглашений с помощью атрибутов
public class ResourceInformation
{
public string Google_Web_ResourceString
{
get { return "http://www.google.com/ig/api?weather="; }
}
public string Google_File_ResourceString
{
get { return @".\GoogleWeather.txt"; }
}
public string WeatherUnderground_Web_ResourceString
{
get { return "http://api.wunderground.com/api/96863c0d67baa805/conditions/q/"; }
}
[Export("ResourceInfo")]
[ExportMetadata("ResourceAffiliation", "WeatherUnderground")]
[ExportMetadata("ResourceLocation", "File")]
public string WunderGround_File_ResourceString
{
get { return @".\Wunder.txt"; }
}
}
Как видно на рис. 10, первая часть соглашения некорректна согласно спецификации именования. Однако, явно добавляя имя контракта и корректные метаданные, вы можете переопределить или добавить что-то в фрагменты, распознаваемые RegistrationBuilder, что делает MEF-атрибуты эффективным средством для задания исключений в соглашениях, определенных RegistrationBuilder.
Бесшовная разработка
В этой статье я рассмотрел конфигурацию, управляемую соглашениями, — новую возможность MEF, предоставляемую классом RegistrationBuilder, который значительно упрощает разработки, связанные с MEF. Бета-версии этих библиотек вы найдете на mef.codeplex.com. Если у вас еще нет .NET Framework 4.5, вы можете зайти на сайт CodePlex и скачать пакет с этой инфраструктурой.
Как ни парадоксально, RegistrationBuilder может сделать ваши повседневные задачи разработки менее связанными с MEF, а использование MEF в ваших проектах — совершенно бесшовным. Отличный пример тому — пакет интеграции, встроенный в Model-View-Controller (MVC) для MEF, о котором вы можете прочитать в блоге группы BCL по ссылке bit.ly/ysWbdL. Если в двух словах, то вы можете скачать пакет в свое MVC-приложение, и это приведет к настройке вашего проекта на использование MEF. Главное заключается в том, что любой имеющийся у вас код будет «просто работать», а когда вы начнете следовать заданному соглашению, вы получите преимущества от использования MEF в своем приложении, не написав самостоятельно ни одной строки MEF-кода. Узнать больше на эту тему вы можете в блоге группы BCL по ссылке bit.ly/ukksfe.