На переднем крае

Расширяемость приложений: инфраструктуры MEF и IoC

Дино Эспозито

image: Julie Lerman В Microsoft .NET Framework 4 появился один интересный новый компонент — хороший ответ на извечный вопрос: как писать расширяемые приложения, способные в период выполнения распознавать все части, из которых они состоят?

Как пояснил в своей статье Глен Блок (Glenn Block) «Building Composable Apps in .NET 4 with the Managed Extensibility Framework» за февраль 2010 г. (msdn.microsoft.com/magazine/ee291628), с помощью Managed Extensibility Framework (MEF) можно упростить построение составляемых (composable) и основанных на плагинах приложений. Как один из тех, кто начал искать подходы к этой проблеме еще в 1994 г. (да, это была одна из первых настоящих задач для меня как для разработчика), я, безусловно, приветствую любые предлагаемые решения в этой области.

MEF не требует приобретать или скачивать какие-либо дополнительные библиотеки и ссылаться на них, а предлагает простой интерфейс программирования, так как эта инфраструктура предназначена для универсальных целей расширения существующих приложений сторонними компонентами. Статья Глена является отличным введением в MEF, и ее следует считать обязательной для прочтения, если вы намереваетесь приступить к созданию приложений, основанных на плагинах (подключаемых модулях).

В этой статье я поясню этапы, необходимые для создания расширяемого приложения с применением MEF в качестве нижележащего уровня, который «склеивает» основное тело приложения и его внешние части.

От IoC к MEF и обратно

Однако, прежде чем перейти к приложению-примеру, я хотел бы поделиться некоторыми мыслями насчет MEF и другого популярного семейства инфраструктур: Inversion of Control (IoC).

Если в двух словах, то функциональность MEF и функциональность типичной инфраструктуры IoC перекрываются, но не совпадают. Большинство инфраструктур IoC позволяет выполнять задачи, которые просто не поддерживаются MEF. Вероятно, вы могли бы задействовать IoC-контейнер с богатой функциональностью и с некоторыми усилиями эмулировать некоторые, специфичные для MEF возможности. В связи с этим, когда я упоминаю о MEF на семинарах и в повседневной работе, меня часто спрашивают: какая разница между MEF и IoC? И когда реально нужна MEF?

Мое мнение таково, что на базовом уровне MEF — это инфраструктура IoC, встроенная прямо в .NET Framework. Она не столь мощная, как многие из нынешних популярных инфраструктур IoC, но позволяет весьма неплохо выполнять основные функции типичного IoC-контейнера.

Сегодня инфраструктурам IoC свойственны три типичные возможности. Во-первых, они могут действовать в качестве фабрики графа объектов и проходить по цепочке отношений и зависимостей объектов, чтобы создать экземпляр любого требуемого и зарегистрированного типа. Во-вторых, инфраструктура IoC способна управлять сроками жизни созданных экземпляров и поддерживает кеширование и пулы. В-третьих, большинство инфраструктур IoC поддерживает перехват и создание динамических прокси для экземпляров специфических типов, чтобы разработчики могли выполнять пред- и постобработку при вызове методов. О перехвате в Unity 2.0 я уже рассказывал (msdn.microsoft.com/magazine/gg535676).

В известной мере MEF тоже может выступать в роли фабрики графа объектов:она способна распознавать и обрабатывать члены класса, которые нужно разрешать в период выполнения. Кроме того, MEF обеспечивает минимальную поддержку кеширования экземпляров, т. е. некоторые возможности кеширования в эту инфраструктуру заложены, но они не столь обширны и функциональны, как в ряде инфраструктур IoC. Наконец, в версии, которая встроена в .NET Framework 4, MEF напрочь лишена средств перехвата.

В каких же случаях стоит использовать MEF? Если вы никогда не работали с какой-либо инфраструктурой IoC и вам нужно лишь улучшить дизайн своей системы, добавив поддержку встраивания зависимостей, то MEF поможет вам легко добиться этой цели. И пока с помощью MEF вам удается быстро решать необходимые задачи, применение данной инфраструктуры предпочтительнее IoC.

С другой стороны, если вы давно работаете с одной или несколькими инфраструктурами IoC и способны выжать из них всю функциональность до последнего бита, тогда MEF вряд ли сможет вам предложить что-либо, кроме, возможно, своей поддержки сканирования различных типов каталогов для поиска подходящих типов. Однако следует отметить, что некоторые инфраструктуры IoC вроде StructureMap (structuremap.net/structuremap/ScanningAssemblies.htm) уже поддерживают сканирование каталогов и сборок для поиска конкретных типов или реализаций определенных интерфейсов. Хотя MEF позволяет делать это, вероятно, легче и более прямолинейно, чем StructureMap (и ряд других).

В целом, первый вопрос, на который нужно ответить, — нужна ли вам универсальная расширяемость. Если да, можно подумать о применении MEF (возможно, в сочетании с IoC, если вам также требуется обработка зависимостей, Singleton-объектов и перехвата). В ином случае лучше задействовать инфраструктуру IoC, если только вам не хватает базовых возможностей MEF. При прочих равных условиях MEF предпочтительнее инфраструктуры IoC, так как MEF встроена прямо в .NET Framework и вам не потребуются никакие дополнительные зависимости.

MEF и расширяемые приложения

Хотя MEF помогает в создании расширяемого приложения, самая тонкая часть работы — проектирование приложения в расчете на расширяемость. Эта задача имеет весьма отдаленное отношение к MEF, IoC или другим технологиям. В частности, вы должны понять, какие части вашего приложения можно сделать доступными плагинам.

Плагином часто является визуальный элемент, а ему нужно взаимодействовать с UI основного приложения, добавлять или расширять меню, создавать панели, отображать диалоговые окна или даже добавлять/масштабировать основные окна. В зависимости от того, как вы представляете себе плагины для своего приложения, информация, которая будет использоваться совместно с плагинами, может состоять только из бизнес-данных (по сути, из некой части текущего состояния приложения) или ссылок на визуальные элементы, например контейнеры, меню, панели инструментов и даже на конкретные элементы управления. Вы группируете эту информацию в какой-либо структуре данных и передаете плагину в момент инициализации. На основе этой информации плагин должен настраивать свой UI и реализовать собственную дополнительную логику.

Далее нужно продумать интерфейс для плагинов. Этот интерфейс зависит от точек встраивания, предусматриваемых в вашем основном приложении. Под «точкой встраивания» я подразумеваю такие места в коде приложения, откуда вы могли бы вызывать плагины, давая им шанс на выполнение.

В качестве примера точки встраивания возьмем Windows Explorer. Как вы, вероятно, знаете, Windows Explorer позволяет расширять свой UI через расширения оболочки. Эти плагины вызываются в очень специфические моменты, скажем, когда пользователь щелкает правой кнопкой мыши выбранный файл, чтобы просмотреть его свойства. Вы — как архитектор приложения — отвечаете за идентификацию этих точек встраивания и за то, какие данные будут передаваться в момент вызова зарегистрированным плагинам.

Прояснив все аспекты дизайна, вы можете поискать такие инфраструктуры, которые упростят вам построение приложения на основе плагинов.

Приложение-пример на основе плагинов

Даже такое простое приложение, как «Find the number» («Найди число»), можно сделать функциональнее и привлекательнее выглядящим с помощью плагинов. На рис. 1 показан основной пользовательский интерфейс приложения. По-видимому, вы захотите создать отдельный проект для определения SDK вашего приложения. Это будет библиотека классов, где вы определите все классы и интерфейсы, необходимые для реализации плагинов. На рис. 2 показан пример.

image: The Simple Sample Application

Рис. 1. Простой пример приложения

Рис. 2. Определения для SDK приложения

public interface IFindTheNumberPlugin {
  void ShowUserInterface(GuessTheNumberSite site);
  void NumberEntered(Int32 number);
  void GameStarted();
  void GameStopped();
}

public interface IFindTheNumberApi {
  Int32 MostRecentNumber { get; }
  Int32 NumberOfAttempts { get; }
  Boolean IsUserPlaying { get; }
  Int32 CurrentLowerBound { get; }
  Int32 CurrentUpperBound { get; }
  Int32 LowerBound { get; }
  Int32 UpperBound { get; }
  void SetNumber(Int32 number);
}

public class FindTheNumberFormBase : Form, IFindTheNumberApi {
  ...
}

Все плагины должны реализовать интерфейс IFindTheNumberPlugin. Основная форма приложения будет наследовать от формы указанного класса, в котором определяется список открытых вспомогательных членов для передачи информации плагинам.

Как вы наверняка догадались по IFindTheNumberPlugin, зарегистрированные плагины вызываются, когда приложение отображает свой UI, когда пользователь предпринимает новую попытку угадать число и когда игра запускается или останавливается. GameStarted и GameStopped — это просто методы уведомления; никакой ввод им не нужен. NumberEntered — уведомление, в котором передается только что введенное пользователем число. Наконец, ShowUserInterface вызывается, когда плагин должен вывести свой UI в окне. В этом случае передается объект-посредник (site object), определенный на рис. 3.

Рис. 3. Объект-посредник для плагинов

public class FindTheNumberSite {
  private readonly FindTheNumberFormBase _mainForm;

  public FindTheNumberSite(FindTheNumberFormBase form) {
    _mainForm = form;
  }

  public T FindElement<T>(String name) where T:class { ... }
  public void AddElement(Control element) { ... }

  public Int32 Height {
    get { return _mainForm.Height; }
    set { _mainForm.Height = value; }
  }

  public Int32 Width { ... }
  public Int32 NumberOfAttempts { ... }
  public Boolean IsUserPlaying { ... }
  public Int32 LowerBound { ... }
  public Int32 UpperBound { ... }
  public void SetNumber(Int32 number) { ... }
}

Объект-посредник представляет точку контакта между плагином и основным приложением (хостом). Плагин должен видеть некую часть состояния хоста и даже иметь возможность модифицировать его UI, но он никогда не должен знать детали внутреннего устройства хоста. Вот почему вы скорее всего захотите создать промежуточный объект-посредник (часть вашей SDK-сборки), на который должны ссылаться проекты плагинов.

Для краткости я опустил реализацию большинства методов на рис. 3, но конструктор объекта-посредника получает ссылку на главное окно приложения и, используя вспомогательные методы с рис. 2 (они предоставляются объектом главного окна), может читать и записывать состояние приложения, а также визуальные элементы. Например, член Height показывает, как плагин может читать и записывать высоту окна хоста.

В частности, метод FindElement позволяет плагину (в приложении-примере) получать конкретный визуальный элемент на форме. Предполагается, что в своем SDK вы открываете некоторые технические детали того, как обращаться к определенным контейнерам, таким как панели инструментов, меню и др. В столь простом приложении-примере предполагается, что вы документируете идентификаторы физических элементов управления. Вот реализация FindElement:

public T FindElement<T>(String name) where T:class {
  var controls = _mainForm.Controls.Find(name, true);
  if (controls.Length == 0)
    return null;
  var elementRef = controls[0] as T;
  return elementRef ?? null;
}
With the design of the application’s extensibility model completed, we’re now ready to introduce the MEF.

Определение импортов для плагинов

Основное приложение предоставляет свойство, в котором перечисляются все зарегистрированные на данный момент плагины. Вот пример:

public partial class FindTheNumberForm : 
  FindTheNumberFormBase {
  public FindTheNumberForm() {
    InitializeMef();
    ...
 }

 [ImportMany(typeof(IFindTheNumberPlugin)]
 public List<IFindTheNumberPlugin> Plugins { 
    get; set; 
  }
  ...
}

Инициализация MEF означает подготовку контейнера композиции (composition container), указывающего каталоги, которые вы намереваетесь использовать, и необязательные провайдеры экспорта. Распространенное решение для приложений для основе плагинов — загрузка плагинов из фиксированного каталога. На рис. 4 показан стартовый код MEF для моего примера.

Рис. 4. Инициализация MEF

private void InitializeMef() {
  try {
    _pluginCatalog = new DirectoryCatalog(@"\My App\Plugins");
    var filteredCatalog = new FilteredCatalog(_pluginCatalog, 
      cpd => cpd.Metadata.ContainsKey("Level") && 
      !cpd.Metadata["Level"].Equals("Basic")); 

    // Create the CompositionContainer with the parts in the catalog
    _container = new CompositionContainer(filteredCatalog);
    _container.ComposeParts(this);
  }
  catch (CompositionException compositionException) {
    ...
  }
  catch (DirectoryNotFoundException directoryException) { 
    ...
  }
}

С помощью DirectoryCatalog вы группируете доступные плагины и используете класс FilteredCatalog (который не является частью MEF, но предлагается как пример в документации MEF; см. bit.ly/gf9xDK) для фильтрации выбранных плагинов. В частности, вы можете потребовать, чтобы у всех загружаемых плагинов был атрибут метаданных, указывающий уровень. При отсутствии этого атрибута плагин игнорируется.

Вызов ComposeParts приводит к заполнению свойства Plugins приложения. Следующий этап — простой вызов плагинов из различных точек встраивания. Первый раз вы вызываете плагины после загрузки приложения, давая им шанс модифицировать UI:

void FindTheNumberForm_Load(Object sender, EventArgs e) {
  // Set up UI
  UserIsPlaying(false);

  // Stage to invoke plugins
  NotifyPluginsShowInterface();
}

void NotifyPluginsShowInterface() {
  var site = new FindTheNumberSite(this);
  if (Plugins == null)
    return;

  foreach (var p in Plugins) {
    p.ShowUserInterface(site);
  }
}

Аналогичные вызовы будут присутствовать в обработчиках событий, срабатывающих при начале новой игры, выходе из текущей игры или просто при новой попытке угадать таинственное число.

Написание примера плагина

Плагин — это класс, который реализует интерфейс расширения, поддерживаемый вашим приложением. Скажем для приложения-примера был бы интересен плагин, показывающий количество предпринятых на данный момент попыток угадать число. Это значение отслеживается прикладной логикой приложения и доступно плагинам через объект-посредник. Плагин должен лишь подготовить свой UI, связать его с предоставляемым значением (текущим количеством попыток) и подключить его к основному окну.

Плагины для приложения-примера будут создавать новые элементы управления в UI главного окна. Пример плагина показан на рис. 5.

Рис. 5. Пример плагина-счетчика

[Export(typeof(IFindTheNumberPlugin))]
[PartMetadata("Level", "Advanced")]
public class AttemptCounterPlugin : IFindTheNumberPlugin {
  private FindTheNumberSite _site;
  private Label _attemptCounterLabel;

  public void ShowUserInterface(FindTheNumberSite site) {
    _site = site;
    var numberToGuessLabelRef = _host.FindElement<Label>("NumberToGuess");
    if (numberToGuessLabelRef == null)
      return;

    // Position of the counter label in the form 
    _attemptCounterLabel = new Label {
      Name = "plugins_AttemptCounter",
      Left = numberToGuessLabelRef.Left,
      Top = numberToGuessLabelRef.Top + 50,
      Font = numberToGuessLabelRef.Font,
      Size = new Size(150, 30),
      BackColor = Color.Yellow,
      Text =  String.Format("{0} attempt(s)", _host.NumberOfAttempts)
    };
    _site.AddElement(_attemptCounterLabel);
  }

  public void NumberEntered(Int32 number = -1) {
    var attempts = _host.NumberOfAttempts;
    _attemptCounterLabel.Text = String.Format("{0} attempt(s)", attempts);
    return;
  }

  public void GameStarted() {
    NumberEntered();
  }

  public void GameStopped() {
  }
}

Плагин создает новый элемент управления Label и размещает его под существующим UI-элементом. Далее, как только плагин получает уведомление о том, что было введено новое число, счетчик обновляется, отражая текущее количество попыток согласно состоянию прикладной логики. На рис. 6 показан работающий плагин.

image: The Sample App and a Few Plug-Ins

Рис. 6. Пример приложения и несколько плагинов

Заключение

В конечном счете самая трудная задача в разработке расширяемых приложений — проектирование хоста и интерфейса плагинов. Это чисто проектировочная задача, решение которой определяется списком функций и требованиями пользователей.

Однако, когда дело доходит до реализации, приходится решать довольно много практических задач независимо от интерфейса плагинов, например как выбирать, загружать и проверять плагины. В этом отношении MEF позволяет значительно упростить создание каталога загружаемых плагинов и автоматически загружать их во многом по аналогии с инфраструктурой IoC.

Заметьте, что MEF постоянно развивается, и новейшую версию, документацию и примеры кода можно найти на mef.codeplex.com.

Дино Эспозито (Dino Esposito) — автор книги «Programming ASP.NET MVC» (Microsoft Press, 2010) и соавтор «Microsoft .NET: Architecting Applications for the Enterprise» (Microsoft Press, 2008). Проживает в Италии и часто выступает на отраслевых мероприятиях по всему миру. С ним можно связаться через его блог weblogs.asp.net/despos.

Выражаю благодарность за рецензирование статьи эксперту Глену Блоку (Glenn Block)