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

в 11:28, , рубрики: .net, microsoft, patterns and practices, Prism, silverlight, wpf, интерфейсы

Оглавление

  1. Введение
  2. Инициализация приложений Prism
  3. Управление зависимостями между компонентами
  4. Разработка модульных приложений
  5. Реализация паттерна MVVM
  6. Продвинутые сценарии MVVM
  7. Создание пользовательского интерфейса
  8. Навигация
  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).
  • Может использовать перехват вызова методов для добавления дополнительной функциональности к целевому объекту.

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 контейнерами, смотрите:

Автор: Unrul

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js