20 April 2013

Руководство разработчика Prism — часть 3, управление зависимостями между компонентами

.NETC#
Translation
Tutorial
Original author: microsoft patterns & practices
Оглавление
  1. Введение
  2. Инициализация приложений Prism
  3. Управление зависимостями между компонентами
  4. Разработка модульных приложений
  5. Реализация паттерна MVVM
  6. Продвинутые сценарии MVVM
  7. Создание пользовательского интерфейса
    1. Рекомендации по разработке пользовательского интерфейса
  8. Навигация
    1. Навигация на основе представлений (View-Based Navigation)
  9. Взаимодействие между слабо связанными компонентами

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

Для связи всех частей воедино, Prism приложения полагаются на DI контейнер. DI контейнеры уменьшают зависимости между объектами, предоставляя способ создания экземпляров классов и управления их временем жизни в зависимости от конфигурации контейнера. При создании объектов с помощью контейнера, он инжектирует в них необходимые зависимости. Если зависимости ещё не были созданы, то контейнер в начале создаёт их и разрешает их собственные зависимости. В некоторых случаях, сам контейнер внедряется как зависимость. Например, при использовании Unity, в модули внедряется контейнер для того, чтобы они могли зарегистрировать в нём свои представления и службы.

Есть несколько преимуществ использования контейнера:
  • Контейнер устраняет потребность компонента определять местоположение его зависимостей или управлять временем их жизни.
  • Контейнер позволяет заменять реализации, не влияя на компоненты.
  • Контейнер облегчает тестируемость, позволяя внедрять в объекты фальшивые зависимости.
  • Контейнер упрощает обслуживание, позволяя легко добавлять к системе новые компоненты.

В контексте приложения, основанного на библиотеке Prism, есть определённые преимущества использования контейнера:
  • Контейнер разрешает зависимости модуля при его загрузке.
  • Контейнер используется для регистрации и создания моделей представления и представлений.
  • Контейнер может создавать модели представления и внедрять представления.
  • Контейнер внедряет службы составного приложения, такие как менеджер регионов, или агрегатор событий.
  • Контейнер используется для регистрации специфичных для модуля служб со специфичной для модуля функциональностью.


Заметка
Некоторые примеры в руководстве Prism используют контейнер Unity Application Block (Unity). Другие, например Modularity QuickStarts, используют Managed Extensibility Framework (MEF). Библиотека самого Prism не зависима от применяемого контейнера, и вы можете использовать её службы и паттерны с другими контейнерами, такими как CastleWindsor, Autofac, Structuremap, Spring.NET, или с любым другим.

Ключевое решение: выбор контейнера внедрения зависимостей


Библиотека Prism предоставляет два DI контейнера по умолчанию: Unity и MEF. Prism расширяема, таким образом вы можете использовать другие контейнеры, написав небольшое количество кода для их адаптации. И Unity, и MEF обеспечивают одинаковую основную функциональность, необходимую для внедрения зависимостей, даже учитывая то, что они работают сильно по-разному. Некоторые из возможностей, предоставляемые обоими контейнерами:
  • Оба позволяют регистрировать типы в контейнере.
  • Оба позволяют регистрировать экземпляры в контейнере.
  • Оба позволяют принудительно создавать экземпляры зарегистрированных типов.
  • Оба внедряют экземпляры зарегистрированных типов в конструкторы.
  • Оба внедряют экземпляры зарегистрированных типов в свойства.
  • У них обоих есть декларативные атрибуты для управления типами и зависимостями.
  • Они оба разрешают зависимости в графе объектов.

Unity предоставляет несколько возможностей, которых нет в MEF:
  • Разрешает конкретные типы без регистрации.
  • Разрешает открытые обобщения (Generics).
  • Может использовать перехват вызова методов для добавления дополнительной функциональности к целевому объекту (Interception).

MEF предоставляет несколько возможностей, которых нет в Unity:
  • Самостоятельно обнаруживает сборки в каталоге файловой системы.
  • Загружает XAP файлы и ищет в них сборки.
  • Проводит рекомпозицию свойств и коллекций при обнаружении новых типов.
  • Автоматически экспортирует производные типы.
  • Поставляется вместе с .NET Framework, начиная с четвёртой версии.

Контейнеры различаются по возможностям и работают по-разному, но библиотека Prism может работать с любым контейнером, обеспечивая одинаковую функциональность. Рассматривая, какой контейнер использовать, имейте в виду свой предыдущий опыт и определите, какой контейнер лучше подходит для сценариев вашего приложения.

Соображения по использованию контейнера


Что следует рассмотреть перед использованием контейнеров:
  • Рассмотрите, уместно ли регистрировать и разрешать компоненты, используя контейнер:
    • Рассмотрите, является ли воздействие на производительность при регистрации в контейнере и разрешении экземпляров, приемлемым для вашего случая. Например, если вы должны создать 10000 многоугольников, чтобы нарисовать что-то внутри метода отрисовки, то создание всех многоугольников через контейнер может привести к существенной потере производительности.
      Заметка.
      Некоторые контейнеры способны разрешать экземпляры объектов почти так же быстро, как и их создание через ключевое слово new. Но, в любом случае, разрешение через контейнер большого количества объектов в цикле, должно быть серьёзно обосновано.
    • Если присутствует множество глубоких зависимостей, то затраты по времени на их разрешение могут существенно возрасти.
    • Если компонент не имеет зависимостей или сам не является зависимостью для других типов, возможно, не имеет смысла пользоваться контейнером при его создании, или помещать его в контейнер, соответственно.
    • Если у компонента есть единственный набор зависимостей, которые являются неотъемлемой его частью и никогда не будут изменяться, возможно, не имеет смысла пользоваться контейнером при его создании. Хотя, в этом случае, его тестирование может усложниться.

  • Рассмотрите, должен ли компонент быть зарегистрирован как синглтон, или как экземпляр:
    • Если компонент является глобальной службой, которая действует как менеджер единственного ресурса, например служба протоколирования, то можно зарегистрировать его как синглтон.
    • Если компонент даёт доступ к общему состоянию многочисленным потребителям, то его можно зарегистрировать как синглтон.
    • Если объект нуждается в создании нового экземпляра каждый раз при внедрении, то его нельзя регистрировать как синглтон. Например, каждое представление, вероятно, нуждается в новом экземпляре модели представления.

  • Рассмотрите, хотите ли вы конфигурировать контейнер в коде или через файл конфигурации:
    • Если вы хотите централизованно управлять всеми службами, используйте файл конфигурации.
    • Если вы хотите регистрировать различные службы в зависимости от каких-либо обстоятельств, конфигурируйте контейнер в коде.
    • Если у вас есть службы уровня модуля, конфигурируйте контейнер через код так, чтобы они были зарегистрированы только при загрузке модуля.


Заметка
Некоторые контейнеры, такие как MEF, не могут быть сконфигурированы через конфигурационный файл и должны быть сконфигурированы в коде.

Базовые сценарии


Контейнеры используются для двух основных целей, а именно: регистрация и разрешение.

Регистрация


Прежде, чем можно будет внедрить зависимости в объект, типы зависимостей должны быть зарегистрированы в контейнере. Регистрация типа обычно включает передачу контейнеру интерфейса и конкретного типа, который реализует этот интерфейс. Есть, прежде всего, два способа регистрации типов и объектов: в коде, или через файл конфигурации. Детали реализации могут изменяться в зависимости от контейнера.

Как правило, есть два способа зарегистрировать типы и объекты в контейнере в коде:
  • Можно зарегистрировать тип, или отображение одного типа в другой. В подходящее время контейнер создаст экземпляр типа, который вы задали.
  • Можно зарегистрировать существующий экземпляр объекта, как синглтон. Контейнер возвратит ссылку на существующий объект.


Регистрация типов в UnityContainer

Во время инициализации, тип может зарегистрировать другие типы, такие как представления и службы. Регистрация позволяет разрешать их зависимости контейнером и стать доступными другим типам. Чтобы сделать это, необходимо внедрить контейнер в конструктор модуля. Следующий код показывает, как OrderModule из Commanding QuickStart регистрирует тип репозитория при инициализации, как синглтон.

public class OrderModule : IModule {
    public void Initialize() {
        this.container.RegisterType<IOrdersRepository, OrdersRepository>(new ContainerControlledLifetimeManager());
        ...
    }
    ...
}

В зависимости от того, какой контейнер вы используете, регистрация может также быть выполнена вне кода через файл конфигурации. Для примера смотрите, «Registering Modules using a Configuration File» в Главе 4, "Modular Application Development."

Регистрация типов с контейнером MEF

Для регистрации типов в контейнере, MEF использует систему, основанную на атрибутах. В результате довольно легко добавить регистрацию типа к контейнеру: для этого требуется добавить атрибут [Export] к типу, который вы хотите зарегистрировать в контейнере, как показано в следующем примере.

[Export(typeof(ILoggerFacade))]
public class CallbackLogger: ILoggerFacade {
   ...
}

Другим вариантом использования MEF, может быть создание экземпляра класса и регистрация именно этого экземпляра в контейнере. QuickStartBootstrapper в Modularity for Silverlight with MEF QuickStart показывает пример этого в методе ConfigureContainer.

protected override void ConfigureContainer() {
    base.ConfigureContainer();
    // Поскольку мы создали CallbackLogger, и он должен использоваться сразу,
    // мы проводим его композицию, чтобы удовлетворить любой импорт (зависимость), который он имеет.
    this.Container.ComposeExportedValue<CallbackLogger>(this.callbackLogger);
}

Заметка
При использовании MEF как контейнера рекомендуется использование именно атрибутов для регистрации типов.

Разрешение


После того, как тип зарегистрирован, он может быть разрешён или внедрён как зависимость. Когда тип разрешается, и контейнер должен создать новый экземпляр этого типа, то он внедряет зависимости в этот экземпляр.

Вообще, когда тип разрешается, происходит одна из трёх вещей:
  • Если тип не был зарегистрирован, контейнер выдаёт исключение.
    Заметка
    Некоторые контейнеры, включая Unity, позволяют разрешать конкретный тип, который не был зарегистрирован.
  • Если тип был зарегистрирован как синглтон, контейнер возвращает экземпляр синглтона. Если это первый вызов, контейнер может создать экземпляр и сохранить его для будущих вызовов.
  • Если тип не был зарегистрирован как синглтон, контейнер возвращает новый экземпляр.
    Заметка
    По умолчанию, типы, зарегистрированные в MEF, являются синглтонами, и контейнер хранит ссылки на объекты. В Unity, по умолчанию, возвращаются новые экземпляры объектов, и контейнер не сохраняет на них ссылок.

Разрешение экземпляров в Unity

Следующий пример кода из Commanding QuickStart показывает, как представления OrdersEditorView и OrdersToolBar разрешаются из контейнера для привязки их к соответствующим регионам.

public class OrderModule : IModule {
    public void Initialize() {
        this.container.RegisterType<IOrdersRepository, OrdersRepository>(new ContainerControlledLifetimeManager());

        // Показываем представление Orders Editor в главном регионе оболочки.
        this.regionManager.RegisterViewWithRegion("MainRegion",
                                                    () =>; this.container.Resolve<OrdersEditorView>());

        // Показываем представление Orders Toolbar в регионе панели инструментов.
        this.regionManager.RegisterViewWithRegion("GlobalCommandsRegion",
                                                    () => this.container.Resolve<OrdersToolBar>());
    }
    ...
}

Конструктор OrdersEditorPresentationModel содержит следующие зависимости (репозиторий заказов и прокси команды заказов), которые вводятся при его разрешении.

public OrdersEditorPresentationModel(IOrdersRepository ordersRepository, OrdersCommandProxy commandProxy) {
    this.ordersRepository = ordersRepository;
    this.commandProxy     = commandProxy;

    // Создание фиктивных данных о заказе.
    this.PopulateOrders();

    // Инициализация CollectionView для основной коллекции заказов.
#if SILVERLIGHT
    this.Orders = new PagedCollectionView( _orders );
#else
    this.Orders = new ListCollectionView( _orders );
#endif

    // Отслеживание текущего выбора.
    this.Orders.CurrentChanged += SelectedOrderChanged;
    this.Orders.MoveCurrentTo(null);
}

В дополнение к внедрению в конструктор, как показано в предыдущем примере, Unity также может внедрять зависимости в свойства. Любые свойства, к которым применён атрибут [Dependency], автоматически разрешаются и внедряются, при разрешении объекта. Если свойство помечено атрибутом OptionalDependency, то при невозможности разрешить зависимость, свойству присваивается null и исключение не генерируется.

Разрешение экземпляров в MEF

Следующий пример кода показывает, как Bootstrapper в Modularity for Silverlight with MEF QuickStart получает экземпляр оболочки. Вместо того чтобы запросить конкретный тип, код мог бы запросить экземпляр интерфейса.

protected override DependencyObject CreateShell() {
    return this.Container.GetExportedValue<Shell>();
}

В любом классе, который разрешается MEF, можно также использовать инжекцию в конструктор, как показано в следующем примере кода из ModuleA в Modularity for Silverlight with MEF QuickStart, у которого внедряются ILoggerFacade и IModuleTracker.

[ImportingConstructor]
public ModuleA(ILoggerFacade logger, IModuleTracker moduleTracker) {
    if (logger == null) {
        throw new ArgumentNullException("logger");
    } 
    if (moduleTracker == null) {
        throw new ArgumentNullException("moduleTracker");
    }
	
    this.logger = logger;
    this.moduleTracker = moduleTracker;
    this.moduleTracker.RecordModuleConstructed(WellKnownModuleNames.ModuleA);
}

С другой стороны, можно использовать инжекцию свойства, как показано в классе ModuleTracker из Modularity for Silverlight with MEF QuickStart, у которого есть экземпляр внедряемого ILoggerFacade.

[Export(typeof(IModuleTracker))]
public class ModuleTracker : IModuleTracker {
     // Из-за ограничений Silverlight/MEF, поле должно быть общедоступно.
     [Import] public ILoggerFacade Logger;
}

Заметка
В Silverlight импортируемые свойства и поля должны быть общедоступными.

Использование контейнеров внедрения зависимостей и служб в Prism


Контейнеры внедрения зависимости, используются, чтобы удовлетворить зависимости между компонентами. Удовлетворение этих зависимостей обычно включает регистрацию и разрешение. Библиотека Prism предоставляет поддержку для контейнеров Unity и MEF, но не зависит от них. Поскольку библиотека имеет доступ к контейнеру через интерфейс IServiceLocator, контейнер может быть легко заменён. Чтобы сделать это, вы должны реализовать интерфейс IServiceLocator. Обычно, если вы замените контейнер, то вы должны будете также написать свой собственный контейнерно-специфичный загрузчик. Интерфейс IServiceLocator определяется в Common Service Locator Library. Это open source проект по обеспечению абстракции контейнеров IoC (Inversion of Control), таких как контейнеры внедрения зависимостей, и локаторы службы. Цель использования этой библиотеки состоит в том, чтобы использовать IoC и Service Location, без предоставления определённой реализации контейнера.

Библиотека Prism предоставляет UnityServiceLocatorAdapter и MefServiceLocatorAdapter. Оба адаптера реализуют интерфейс ISeviceLocator, расширяя тип ServiceLocatorImplBase. Следующая иллюстрация показывает иерархию классов.

Реализации Common Service Locator в Prism.

Хотя библиотека Prism не ссылается и не полагается на определённый контейнер, для приложения характерно использовать вполне конкретный DI контейнер. Это означает, что для приложения разумно ссылаться на определённый контейнер, но библиотека Prism не ссылается на контейнер непосредственно. Например, приложение Stock Trader RI и несколько из QuickStarts, используют Unity в качестве контейнера. Другие примеры и QuickStarts используют MEF.

IServiceLocator


Следующий код показывает интерфейс IServiceLocator и его методы.

public interface IServiceLocator : IServiceProvider {
    object GetInstance(Type serviceType);
    object GetInstance(Type serviceType, string key);
    IEnumerable<object> GetAllInstances(Type serviceType);
    TService GetInstance<TService>();
    TService GetInstance<TService>(string key);
    IEnumerable<TService> GetAllInstances<TService>();
}

Service Locator дополняет библиотеку Prism методами расширения, показанными в следующем коде. Можно увидеть, что IServiceLocator используется только для разрешения, а не для регистрации.

public static class ServiceLocatorExtensions {
    public static object TryResolve(this IServiceLocator locator, Type type) {
        try {
            return locator.GetInstance(type);
        }
        catch (ActivationException) {
            return null;
        }
    }

    public static T TryResolve<T>(this IServiceLocator locator) where T: class {
        return locator.TryResolve(typeof(T)) as T;
    }
}

Метод расширения TryResolve, который контейнер Unity не поддерживает, возвращает экземпляр типа, который должен быть разрешён, если он было зарегистрирован, иначе он возвращает null.

ModuleInitializer использует IServiceLocator для того, чтобы разрешить зависимости модуля во время его загрузки, как показано в следующих примерах кода.

IModule moduleInstance = null;
try {
    moduleInstance = this.CreateModule(moduleInfo);
    moduleInstance.Initialize();
}
...

protected virtual IModule CreateModule(string typeName) {
    Type moduleType = Type.GetType(typeName);
    if (moduleType == null) {
        throw new ModuleInitializeException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.FailedToGetType, typeName));
    }
    return (IModule)this.serviceLocator.GetInstance(moduleType);
}

Соображения по использованию IServiceLocator


IServiceLocator не предназначается для использования в качестве контейнера общего назначения. У контейнеров может быть различная семантика использования, которая часто влияет на выбор контейнера. Принимая это во внимание, Stock Trader RI использует контейнер внедрения зависимости непосредственно вместо того, чтобы использовать IServiceLocator. Это является рекомендованным подходом при разработке приложений.

В следующих ситуациях использование IServiceLocator является уместным:
  • Вы — независимый поставщик программного обеспечения (ISV), разрабатывающий стороннюю службу, которая должна поддерживать различные контейнеры.
  • Вы разрабатываете службу, которая будет использоваться в организации, где используются различные контейнеры.

Дополнительная информация


Для получения информации, связанной с DI контейнерами, смотрите:
Tags:c#.netwpfprismsilverlightuser interfacepatterns and practicesmicrosoftunitymefdependency injectionioc контейнеры
Hubs: .NET C#
+11
20.2k 51
Comments 28
Popular right now
C++ Developer. Professional
December 28, 202060,000 ₽OTUS
Рефакторинг кода .NET
December 7, 202030,200 ₽Luxoft Training
Программирование на языке C (Си)
December 14, 202022,990 ₽Специалист.ру
C++ Junior Developer
March 3, 202123,990 ₽Level UP