Как я могу разрешить декораторы для объектов, созданных фабрикой, в Simple Injector

Я покажу большую часть своей сантехники, но я постараюсь свести ее к минимуму, чтобы вопрос оставался простым.

Одна из моих конечных точек API полагается на внешних поставщиков для завершения вызова. Когда пользователь отправляет запрос к этой конечной точке, он может указать, какого поставщика они хотят, чтобы мы использовали при обработке запроса, скажем, поставщиками являются Bing и Google.

Итак, у меня есть IProvider интерфейс и две конкретные реализации BingProvider и GoogleProvider (в моем реальном API интерфейс провайдера на самом деле является универсальным интерфейсом, но я оставляю универсальные шаблоны, чтобы не сделать это более беспорядочным, чем должно быть). Мне нужно определить правильного поставщика на основе поля в запросе. Simple Injector не позволяет регистрировать несколько конкретных реализаций одного и того же интерфейса, поэтому мне приходится использовать фабрику; Я создаю такой, который выглядит примерно так:

public class ProviderFactory
{
    private readonly Func<string, IProvider> _Selector;

    public ProviderFactory(Func<string, IProvider> selector)
    {
        this._Selector = selector;
    }

    public IProvider Get(string provider)
    {
        return this._Selector(provider);
    }
}

Я регистрирую свою фабрику в контейнере, выполнив примерно следующее:

container.RegisterSingleton<ProviderFactory>(new ProviderFactory(provider =>
    {
        switch (provider)
        {
            case "Bing":
                return container.GetInstance<BingProvider>()
            case "Google":
                return container.GetInstance<GoogleProvider>()
            default:
                throw new ArgumentOutOfRangeException("Unknown provider: " + provider);
        }
    }));

Я это тестирую. Оно работает. Фантастика.

Теперь мне нужно создать и зарегистрировать несколько декораторов для моего IProvider. В каждой конкретной реализации IProvider эти декораторы должны применяться, когда контейнер разрешает их. Ради этого примера допустим, что у меня есть Decorator1 и Decorator2, которые реализуют IProvider. Регистрирую их в контейнере вот так:

container.RegisterDecorator(typeof(IProvider), typeof(Decorator1), Lifestyle.Singleton);
container.RegisterDecorator(typeof(IProvider), typeof(Decorator2), Lifestyle.Singleton);

Вот где проблема. Когда моя фабрика разрешает экземпляр BingProvider или GoogleProvider, это именно то, что я получаю. Я хочу получить экземпляр Decorator2, который украшает экземпляр Decorator1, который, в свою очередь, украшает те конкретные реализации IProvider, которые я запрашивал. Я предполагаю, что это связано с тем, что я специально не прошу контейнер разрешить экземпляр IProvider, а скорее прошу его разрешить конкретную реализацию IProvider.

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

Изменить

Хорошо, подумав еще немного, я понимаю, почему декораторы никогда не будут разрешены. Возьмите, например, Decorator1 и BingProvider. И Decorator1, и BingProvider реализуют IProvider, но Decorator1 не реализуют BingProvider, поэтому, когда я прошу Simple Injector разрешить экземпляр BingProvider, он даже не может дать мне экземпляр Decorator1. Я собираюсь попросить его каким-то образом разрешить экземпляр IProvider, но дать мне правильную конкретную реализацию с установленными декораторами.

Отлично, что я понимаю, но теперь я менее уверен в том, что делать дальше.

Обновить

Основываясь на ответе Стивена, я изменил свою заводскую регистрацию следующим образом:

var bingProvider = Lifestyle.Singleton
    .CreateProducer<IProvider, BingProvider>(container);

var googleProvider = Lifestyle.Singleton
    .CreateProducer<IProvider, GoogleProvider>(container);

container.RegisterSingleton<ProviderFactory>(new ProviderFactory(provider =>
    {
        switch (provider)
        {
            case "Bing":
                return bingProvider.GetInstance();
            case "Google":
                return googleProvider.GetInstance();
            default:
                throw new ArgumentOutOfRangeException("Unknown provider: " + provider);
        }
    }));

Моя новая проблема заключается в том, что когда я запускаю свой модульный тест для проверки контейнера, тест завершается с ошибкой, которая выглядит примерно так (мне пришлось исправить это сообщение об ошибке, чтобы оно соответствовало моему примеру здесь, надеюсь, это не приведет к тому, что что-то будет утрачено при переводе):

Конфигурация недействительна. Были получены следующие диагностические предупреждения:
- [Torn Lifestyle] Регистрация для IProvider соответствует той же реализации и образу жизни, что и регистрация для IProvider. Оба они сопоставляются с Decorator1 (Singleton). Это приведет к тому, что каждая регистрация будет разрешаться в другой экземпляр: каждая регистрация будет иметь свой собственный экземпляр.
- [Torn Lifestyle] Регистрация для IProvider соответствует той же реализации и образу жизни, что и регистрация для IProvider. Оба они сопоставляются с Decorator2 (Singleton). Это приведет к тому, что каждая регистрация будет разрешаться в другой экземпляр: каждая регистрация будет иметь свой собственный экземпляр.
См. свойство "Ошибка" для получения подробной информации о предупреждениях. См. https://simpleinjector.org/diagnostics, как исправить проблемы и как подавить отдельные предупреждения.


person Jason Boyd    schedule 27.04.2016    source источник
comment
Я мало что знаю о простом инжекторе. Но такая ситуация (несколько реализаций одного и того же интерфейса) является примером того, почему Pure DI, на мой взгляд, лучше, чем использование контейнера DI. Взгляните на эту статью, чтобы узнать больше об этом аргументе.   -  person Yacoub Massad    schedule 27.04.2016


Ответы (1)


Simple Injector не позволяет регистрировать несколько конкретных реализаций одного и того же интерфейса.

Это утверждение неверно. На самом деле есть несколько способов сделать это. Я думаю, что три наиболее распространенных способа сделать это:

  1. Использовать условную регистрацию
  2. Зарегистрируйте коллекцию типов
  3. Вручную создайте InstanceProducer экземпляров.

Варианты 1 и 3 кажутся наиболее подходящими в вашем случае, поэтому давайте начнем с варианта 3: Создание InstanceProducers:

// Create two providers for IProvider according to the required lifestyle.
var bing = Lifestyle.Singleton.CreateProducer<IProvider, BingProvider>(container);
var google = Lifestyle.Singleton.CreateProducer<IProvider, GoogleProvider>(container);

container.RegisterSingleton<ProviderFactory>(new ProviderFactory(provider => {
    switch (provider) {
        case "Bing": return bing.GetInstance();
        case "Google": return google.GetInstance();
        default: throw new ArgumentOutOfRangeException();
    }
}));

Здесь мы создаем два InstanceProducer экземпляра, по одному для каждого IProvider. Важной частью здесь является создание производителя для абстракции IProvider, поскольку это позволяет применять декораторы для IProvider.

В качестве альтернативы вы можете переместить оператор _8 _-_ 9_ внутрь ProviderFactory и снабдить его двумя отдельными делегатами; по одному на каждого провайдера. Например:

public ProviderFactory(Func<IProvider> bingProvider, Func<IProvider> googleProvider) { .. }

public IProvider Get(string provider) {
    switch (provider) {
        case "Bing": return bingProvider();
        case "Google": return googleProvider();
        default: throw new ArgumentOutOfRangeException();
    }
}

Регистрация очень похожа на предыдущую:

var bing = Lifestyle.Singleton.CreateProducer<IProvider, BingProvider>(container);
var google = Lifestyle.Singleton.CreateProducer<IProvider, GoogleProvider>(container);

container.RegisterSingleton<ProviderFactory>(new ProviderFactory(
    bingProvider: bing.GetInstance,
    googleProvider: google.GetInstance));

Вместо того, чтобы вводить Func<T> делегатов в фабрику, в зависимости от ваших потребностей, вы можете напрямую внедрить IProvider. Это означает, что ваш конструктор будет выглядеть следующим образом:

public ProviderFactory(IProvider bing, IProvider google) { ... }

Теперь вы можете использовать условную регистрацию для IProvider, чтобы устранить двусмысленность аргументов конструктора:

container.RegisterSingleton<ProviderFactory>();
container.RegisterConditional<IProvider, BingProvider>(
    c => c.Consumer.Target.Name == "bing");
container.RegisterConditional<IProvider, GoogleProvider>(
    c => c.Consumer.Target.Name == "google");

Преимущество этого в том, что вы не откладываете создание графа объекта; все поставщики вводятся напрямую после разрешения потребителя фабрики.

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

Такой диспетчер будет очень похож на сам интерфейс IProvider, но добавляет параметр string provider к своим методам экземпляра. Например:

interface IProviderDispatcher {
    void DoSomething(string provider, ProviderData data);
}

Где реализация диспетчера может выглядеть так:

public ProviderDispatcher(IProvider bing, IProvider google) { .. }

public void DoSomething(string provider, ProviderData data) {
    this.Get(provider).DoSomething(data);
}

private IProvider Get(string provider) {
    switch (provider) {
        case "Bing": return this.bing;
        case "Google": return this.google;
        default: throw new ArgumentOutOfRangeException();
    }
}

Решение для диспетчера может быть таким же af для фабрики, но теперь мы скрываем лишний шаг от потребителя.

Было бы даже лучше, если бы мы могли полностью удалить IProviderDispatcher абстракцию, но это возможно только в том случае, если string provider данные времени выполнения являются контекстными данными, доступными во время запроса. В этом случае мы могли бы сделать следующее:

interface IProviderContext {
    string CurrentProvider { get; }
}

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

class ProviderDispatcherProxy : IProvider {
    public ProviderDispatcherProxy(Func<IProvider> bingProvider, 
        Func<IProvider> googleProvider,
        IProviderContext providerContext) { ... }

    void IProvider.DoSomething(ProviderData data) {
        // Dispatch to the correct provider
        this.GetCurrentProvider.DoSomething(data);
    }

    private IProvider GetCurrentProvider() =>
        switch (this.providerContext.CurrentProvider) {
            case "Bing": return this.bingProvider();
            case "Google": return this.googleProvider();
            default: throw new ArgumentOutOfRangeException();
        }
    };
}

class AspNetProviderContext : IProviderContext {
    public CurrentProvider => HttpContext.Current.Request.QueryString["provider"];
}

Опять же, внутренне он по-прежнему очень похож на предыдущий, но теперь, поскольку значение поставщика - это то, что мы можем решить из доступного внешнего контекста (HttpContext.Current), мы сможем позволить потребителю работать с IProvider напрямую. Вы можете зарегистрировать это следующим образом:

container.RegisterSingleton<IProviderContext>(new AspNetProviderContext());

var bing = Lifestyle.Singleton.CreateProducer<IProvider, BingProvider>(container);
var google = Lifestyle.Singleton.CreateProducer<IProvider, GoogleProvider>(container);
container.RegisterSingleton<IProvider>(new ProviderDispatcherProxy(
    bingProvider: bing.GetInstance,
    googleProvider: google.GetInstance));

Теперь вы можете просто ввести IProvider своим потребителям, и отправка будет происходить автоматически в фоновом режиме.

person Steven    schedule 27.04.2016
comment
Там есть что переварить. Я пошел с созданием поставщиков экземпляров вручную, потому что это помогло мне быстрее всех запустить. Сейчас я пытаюсь разобраться с проблемой Torn Lifestyle, которая возникает из-за декораторов. - person Jason Boyd; 27.04.2016
comment
@JasonBoyd, можете ли вы обновить свой вопрос и показать свои регистрации и точное сообщение, которое вы получаете? - person Steven; 27.04.2016
comment
Я пытаюсь зарегистрировать образ жизни декораторов как Singleton, но мне приходит в голову, что это может не иметь значения, каков образ жизни декораторов. Если IProvider, который украшается, вводится в объект с Singleton образом жизни, разве это не сделает декораторов одинаковыми? Или, другими словами, только один тип зависит от IProvider, и экземпляр этого типа будет только один раз, и мне действительно не нужно беспокоиться об образе жизни декораторов или IProvider в этом отношении. - person Jason Boyd; 28.04.2016
comment
@JasonBoyd Я не могу объяснить поведение, которое вы наблюдаете. Simple Injector должен игнорировать разорванные предупреждения об образе жизни на подобных декораторах по очевидным причинам. Я не могу воспроизвести это локально. Это могло быть ошибкой. Можете ли вы переместить эту проблему в Github с минимальным количеством кода, чтобы точно воспроизвести проблему? - person Steven; 28.04.2016
comment
@JasonBoyd: Но обходной путь, который вы можете использовать прямо сейчас, - это сделать ваши декораторы временными и вставить Func<T> в фабрику / диспетчер / прокси. - person Steven; 28.04.2016
comment
Я боялся, что какое-то время не смогу воссоздать это. Но я наконец понял это. Это происходит только тогда, когда у меня более одного декоратора. Я опубликую что-нибудь на GitHub. - person Jason Boyd; 28.04.2016