Как украсить класс, который полагается на значение времени выполнения для создания

Я новичок в использовании Simple Injector, хотя уже давно использую Ninject, поэтому в целом мне комфортно с DI. Одна вещь, которая привлекла меня к использованию Simple Injector, — это простота использования декораторов.

Мне удалось успешно использовать декораторы с Simple Injector во всех обычных случаях, когда зависимости разрешаются при запросе службы. Однако мне трудно понять, есть ли способ применить мои декораторы в случае, когда сервис должен быть создан с использованием значения времени выполнения.

В Ninject я мог передать ConstructorArgument запросу kernel.Get<IService>, который мог быть унаследован по цепочке из N декораторов вплоть до «настоящего» реализующего класса. Я не могу найти способ воспроизвести это с помощью Simple Injector.

Я поместил очень простой код ниже, чтобы проиллюстрировать. Что бы я хотел сделать в реальном мире, так это передать экземпляр IMyClassFactory другим классам в моем приложении. Затем эти другие классы могли использовать его для создания IMyClass экземпляров, используя IRuntimeValue, которые они предоставили. Экземпляр IMyClass, полученный от IMyClassFactory, будет автоматически украшен зарегистрированными декораторами.

Я знаю, что мог бы вручную применить мой декоратор(ы) в моем IMyClassFactory или любом Func<IMyClass>, который я мог бы придумать, но я хотел бы, чтобы он "просто работал".

Я продолжаю ходить вокруг да около, пытаясь абстрагироваться от конструкции MyClass, но я не могу понять, как заставить ее разрешаться с помощью аргумента конструктора IRuntimeValue и украшаться.

Я упускаю из виду очевидное решение?

using System;
using SimpleInjector;
using SimpleInjector.Extensions;

public class MyApp
{
    [STAThread]
    public static void Main()
    {
        var container = new Container();
        container.Register<IMyClassFactory, MyClassFactory>();
        container.RegisterDecorator(typeof (IMyClass), typeof (MyClassDecorator));

        container.Register<Func<IRuntimeValue, IMyClass>>(
                 () => r => container.GetInstance<IMyClassFactory>().Create(r));

        container.Register<IMyClass>(() =>  ?????));  // Don't know what to do

        container.GetInstance<IMyClass>(); // Expect to get decorated class
    }
}

public interface IRuntimeValue
{
}

public interface IMyClass
{
    IRuntimeValue RuntimeValue { get; }
}

public interface IMyClassFactory
{
    IMyClass Create(IRuntimeValue runtimeValue);
}

public class MyClassFactory : IMyClassFactory
{
    public IMyClass Create(IRuntimeValue runtimeValue)
    {
        return new MyClass(runtimeValue);
    }
}

public class MyClass : IMyClass
{
    private readonly IRuntimeValue _runtimeValue;

    public MyClass(IRuntimeValue runtimeValue)
    {
        _runtimeValue = runtimeValue;
    }

    public IRuntimeValue RuntimeValue
    {
        get
        {
            return _runtimeValue;
        }
    }
}

public class MyClassDecorator : IMyClass
{
    private readonly IMyClass _inner;

    public MyClassDecorator(IMyClass inner)
    {
        _inner = inner;
    }

    public IRuntimeValue RuntimeValue
    {
        get
        {
            return _inner.RuntimeValue;
        }
    }
}

Редактировать 1:

Хорошо, спасибо Стивену за отличный ответ. Это дало мне пару идей.

Может быть, чтобы сделать это немного более конкретным (хотя это не моя ситуация, более «классическая»). Скажем, у меня есть ICustomer, который я создаю во время выполнения, читая БД или десериализуя с диска или что-то в этом роде. Так что я думаю, что это будет считаться «новым», если процитировать один из статьи, на которые ссылается Стивен. Я хотел бы создать экземпляр ICustomerViewModel, чтобы я мог отображать и управлять своим ICustomer. Мой конкретный класс CustomerViewModel принимает ICustomer в своем конструкторе вместе с другой зависимостью, которую может разрешить контейнер.

Итак, у меня есть ICustomerViewModelFactory, в котором определен метод .Create(ICustomer customer), который возвращает ICustomerViewModel. Я всегда мог заставить это работать, прежде чем задал этот вопрос, потому что в моей реализации ICustomerViewModelFactory я мог сделать это (фабрика реализована в корне композиции):

return new CustomerViewModel(customer, container.GetInstance<IDependency>());

Моя проблема заключалась в том, что я хотел, чтобы мой ICustomerViewModel был украшен контейнером, и его обновление обошло это стороной. Теперь я знаю, как обойти это ограничение.

Итак, я думаю, что мой дополнительный вопрос: во-первых, мой дизайн неправильный? Я действительно чувствую, что ICustomer следует передать в конструктор CustomerViewModel, потому что это демонстрирует намерение, что оно требуется, проверяется и т. д. Я не хочу добавлять его постфактум.


person Jon Comtois    schedule 12.03.2014    source источник


Ответы (1)


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

Так что в целом есть два решения для этого. Либо вы передаете значение времени выполнения через граф вызова метода, либо создаете «контекстную» службу, которая может предоставить это значение времени выполнения по запросу.

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

В вашем случае это будет означать, что фабрика создает IMyService без передачи IRuntimeValue, и ваш код передает это значение IMyService, используя метод (ы), который он указывает:

var service = _myServiceFactory.Create();
service.DoYourThing(runtimeValue);

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

В этом случае вы должны определить службу, которая может быть внедрена и позволяет получить этот контекст. Например:

public interface IUserContext {
    User CurrentUser { get; }
}

public interface ITimeProvider {
    DateTime Now { get; }
}

В этих случаях текущий пользователь и текущее время вводятся не напрямую в конструктор, а в эти службы. Компонент, которому требуется доступ к текущему пользователю, может просто вызвать _userContext.CurrentUser, и это будет сделано после создания объекта (читай: не внутри конструктора). Итак: в ленивой манере.

Однако это означает, что IRuntimeValue должен быть где-то установлен до вызова MyClass. Это, вероятно, означает, что вам нужно установить его на заводе. Вот пример:

var container = new Container();
var context = new RuntimeValueContext();
container.RegisterSingle<RuntimeValueContext>(context);
container.Register<IMyClassFactory, MyClassFactory>();
container.RegisterDecorator(typeof(IMyClass), typeof(MyClassDecorator));
container.Register<IMyClass, MyClass>();

public class RuntimeValueContext {
    private ThreadLocal<IRuntimeValue> _runtime;
    public IRuntimeValue RuntimeValue {
        get { return _runtime.Value; }
        set { _runtime.Value = value; }
    }
}

public class MyClassFactory : IMyClassFactory {
    private readonly Container _container;
    private readonly RuntimeValueContext context;
    public MyClassFactory(Container container, RuntimeValueContext context) {
        _container = container;
        _context = context;
    }

    public IMyClass Create(IRuntimeValue runtimeValue) {
        var instance = _container.GetInstance<IMyClass>();
        _context.RuntimeValue = runtimeValue;
        return instance;
    }
}

public class MyClass : IMyClass {
    private readonly RuntimeValueContext _context;
    public MyClass(RuntimeValueContext context) {
        _context = context;
    }
    public IRuntimeValue RuntimeValue { get { return _context.Value; } }
}

Вы также можете позволить MyClass принять IRuntimeValue и выполнить следующую регистрацию:

container.Register<IRuntimeValue>(() => context.Value);

Но запрещает проверку графа объектов, поскольку Simple Injector гарантирует, что регистрации никогда не возвращают null, но context.Value по умолчанию будет null. Итак, еще один вариант - сделать следующее:

container.Register<IMyClass>(() => new MyClass(context.Value));

Это позволяет проверить регистрацию IMyClass, но во время проверки все равно будет создан новый экземпляр MyClass, который вводится с нулевым значением. Если у вас есть защитное предложение в конструкторе MyClass, это не удастся. Однако эта регистрация не позволяет контейнеру автоматически связывать MyClass. Автоматическое связывание этого класса может пригодиться, например, когда у вас есть больше зависимостей для внедрения в MyClass.

person Steven    schedule 13.03.2014
comment
Кроме того, если служба зависит от значения времени выполнения, вы вложили в потребление службы некоторые знания о реализации службы, чего вы пытаетесь избежать. Если одна реализация требует передачи числа 42, относится ли этот номер к другой реализации? Что означает 42? - person Lasse V. Karlsen; 13.03.2014
comment
@ LasseV.Karlsen Я полностью согласен с вами в отношении типов значений и тому подобного. Но я пытаюсь разобраться в случае, когда, скажем, значением времени выполнения является ICustomer, который был только что прочитан из БД, и мне нужно передать его (вместе с некоторыми другими зависимостями, разрешенными контейнером) в конструктор, скажем, для CustomerViewModel. Стивен дал мне много размышлений о том, является ли это правильным дизайном в первую очередь. - person Jon Comtois; 13.03.2014
comment
Это довольно много информации для меня, чтобы переварить и обдумать (и спасибо за ссылки и помощь мне попасть в яму успеха). Я собираюсь добавить редактирование для дальнейшего комментария. На самом деле я изначально придумал ваше второе решение самостоятельно, но немного испугался его, потому что 1) я никогда не использовал хранилище ThreadLocal (аккуратно) и 2) я боялся, что могут быть некоторые другие побочные эффекты удержания на ссылка на объект в контексте. Но это может быть мой путь. - person Jon Comtois; 13.03.2014
comment
@jomtois: я не очень хорошо знаком с MVVM, но я думаю, что лучше всего переместить модель из конструктора ViewModel и переместить ее в свойство. Это позволяет фабрике легко разрешать ViewModel и устанавливать модель, используя предоставленное свойство. - person Steven; 13.03.2014
comment
+1 Отличное объяснение проблем, вызванных смешиванием новых и инъекционных материалов, а также ссылка на сообщение в блоге на новое или не новое. Я думаю, что внедрение метода или сеттера для newables является ответом в большинстве случаев, предпочтительнее внедрение метода. Кажется, что каждый раз, когда я расстраиваюсь из-за кажущегося отсутствия гибкости SimpleInjector, я вынужден менять свой дизайн таким образом, чтобы в конечном итоге с ним было лучше и проще. Слава тебе, Стивен! ;) - person Matt Miller; 21.03.2014