依赖注入组合根和装饰器模式

Dependency Injection composition root and decorator pattern

我在使用依赖项注入时在装饰器模式的实现中遇到了 WhosebugException。我认为这是因为我是 "missing" 我对 DI/IoC 的理解。

例如,我目前有 CustomerServiceCustomerServiceLoggingDecorator。 classes 都实现了 ICustomerService,装饰器 class 所做的只是使用注入的 ICustomerService 但添加了一些简单的 NLog 日志记录,这样我就可以使用日志记录而不影响代码CustomerService 同时也不违反单一职责原则。

但是这里的问题是,因为CustomerServiceLoggingDecorator实现了ICustomerService,它还需要注入ICustomerService的实现才能工作,Unity会一直尝试解决它自身导致无限循环,直到溢出堆栈。

这些是我的服务:

public interface ICustomerService
{
    IEnumerable<Customer> GetAllCustomers();
}

public class CustomerService : ICustomerService
{
    private readonly IGenericRepository<Customer> _customerRepository;

    public CustomerService(IGenericRepository<Customer> customerRepository)
    {
        if (customerRepository == null)
        {
            throw new ArgumentNullException(nameof(customerRepository));
        }

        _customerRepository = customerRepository;
    }

    public IEnumerable<Customer> GetAllCustomers()
    {
        return _customerRepository.SelectAll();
    }
}

public class CustomerServiceLoggingDecorator : ICustomerService
{
    private readonly ICustomerService _customerService;
    private readonly ILogger _log = LogManager.GetCurrentClassLogger();

    public CustomerServiceLoggingDecorator(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    public IEnumerable<Customer> GetAllCustomers()
    {
        var stopwatch = Stopwatch.StartNew();
        var result =  _customerService.GetAllCustomers();

        stopwatch.Stop();

        _log.Trace("Querying for all customers took: {0}ms", stopwatch.Elapsed.TotalMilliseconds);
        return result;
    }
}

我目前有这样的注册设置(此存根方法由 Unity.Mvc 创建):

        public static void RegisterTypes(IUnityContainer container)
        {
            // NOTE: To load from web.config uncomment the line below. Make sure to add a Microsoft.Practices.Unity.Configuration to the using statements.
            // container.LoadConfiguration();

            // TODO: Register your types here
            // container.RegisterType<IProductRepository, ProductRepository>();

            // Register the database context
            container.RegisterType<DbContext, CustomerDbContext>();

            // Register the repositories
            container.RegisterType<IGenericRepository<Customer>, GenericRepository<Customer>>();

            // Register the services

            // Register logging decorators
            // This way "works"*
            container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
            new InjectionConstructor(
                new CustomerService(
                    new GenericRepository<Customer>(
                        new CustomerDbContext()))));

            // This way seems more natural for DI but overflows the stack
            container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>();

        }

所以现在我不确定 "proper" 实际创建具有依赖项注入的装饰器的方法。我的装饰器基于 Mark Seemann 的回答 here。在他的示例中,他正在更新几个传递到 class 的对象。这就是我的 it "works"* 片段的工作原理。但是,我想我错过了一个基本的步骤。

为什么要手动创建这样的新对象?这是否否定了让容器为我解析的意义?或者我应该在这个方法中执行 contain.Resolve() (服务定位器),让所有依赖项仍然注入?

我稍微熟悉 "composition root" 概念,即您应该将这些依赖项连接到一个且仅一个位置,然后向下级联到应用程序的较低级别。那么 Unity.Mvc 生成的 RegisterTypes() 是 ASP.NET MVC 应用程序的组合根吗?如果是这样,直接在此处更新对象实际上是正确的吗?

我的印象是,通常使用 Unity 您需要自己创建合成根,但是,Unity.Mvc 是一个例外,它创建了自己的合成根,因为它似乎能够将依赖项注入到控制器中,这些控制器在构造函数中具有诸如 ICustomerService 的接口,而无需我编写代码来实现它。

问题:我想我遗漏了一条关键信息,由于循环依赖,这导致我 WhosebugExceptions。如何在仍然遵循控制原则和约定的依赖性 injection/inversion 的同时正确实施我的装饰器 class?

第二个问题:如果我决定只在某些情况下应用日志装饰器呢?因此,如果我有 MyController1,我希望有一个 CustomerServiceLoggingDecorator 依赖项,但 MyController2 只需要一个正常的 CustomerService,我该如何创建两个单独的注册?因为如果我这样做:

container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>();
container.RegisterType<ICustomerService, CustomerService>();

然后一个将被覆盖,这意味着两个控制器都将注入装饰器或注入普通服务。我如何允许两者兼顾?

编辑:这不是一个重复的问题,因为我在循环依赖方面遇到问题,并且对此缺乏正确的 DI 方法的理解。我的问题适用于整个概念,而不仅仅是像链接问题那样的装饰器模式。

前言

每当您在使用 DI 容器(Unity 或其他)时遇到问题,问问自己:is using a DI Container worth the effort?

在大多数情况下,答案应该是。请改用 Pure DI。用 Pure DI 回答你所有的答案都是微不足道的。

团结

如果您必须使用Unity,以下内容或许会对您有所帮助。我自 2011 年以来就没有使用过 Unity,所以从那以后情况可能发生了变化,但是在 my book 的第 14.3.3 节中查找问题,这样的事情可能会成功:

container.RegisterType<ICustomerService, CustomerService>("custSvc");
container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
    new InjectionConstructor(
        new ResolvedParameter<ICustomerService>("custSvc")));

或者,您也可以这样做:

container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
    new InjectionConstructor(
        new ResolvedParameter<CustomerService>()));

此替代方案更易于维护,因为它不依赖于命名服务,但具有无法通过 ICustomerService 接口解析 CustomerService 的(潜在)缺点。无论如何你可能不应该这样做,所以这应该不是问题,所以这可能是一个更好的选择。

Question: I believe I'm missing a key piece of information, which is leading me to WhosebugExceptions due to circular dependencies. How do I correctly implement my decorator class while still following dependency injection/inversion of control principles and conventions?

正如已经指出的那样,最好的方法是使用以下结构。

container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
    new InjectionConstructor(new ResolvedParameter<CustomerService>()));

这允许您指定如何按类型解析参数。您也可以按名称执行此操作,但按类型执行更清晰,并且允许在编译时进行更好的检查,因为不会捕获字符串中的更改或错误键入。 请注意,此代码部分与 Mark Seemann 提供的代码之间的唯一细微差别是 InjectionConstructor 的拼写更正。我不会再详细说明这部分,因为没有其他要补充的,Mark Seemann 还没有解释。


Second question: What about if I decided I only wanted to apply the logging decorator in certain circumstances? So if I had MyController1 that I wished to have a CustomerServiceLoggingDecorator dependency, but MyController2 only needs a normal CustomerService, how do I create two separate registrations?

您可以使用上面指定的方式使用 Fluent 表示法或使用具有依赖项覆盖的命名依赖项来执行此操作。

流利

这会向容器注册控制器并在构造函数中为该类型指定重载。与第二种方法相比,我更喜欢这种方法,但这仅取决于您要在何处指定类型。

container.RegisterType<MyController2>(
    new InjectionConstructor(new ResolvedParameter<CustomerService>()));

命名依赖项

你以完全相同的方式执行此操作,你可以像这样注册它们。

container.RegisterType<ICustomerService, CustomerService>("plainService");

container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
    new InjectionConstructor(new ResolvedParameter<CustomerService>()));

此处的不同之处在于,您对可以使用同一接口解析的其他类型使用命名依赖项。这是因为每次 Unity 完成解析时,接口都需要解析为一个具体类型,因此您不能将多个未命名的注册类型注册到同一接口。现在您可以使用属性在控制器构造函数中指定覆盖。我的示例是针对名为 MyController2 的控制器,我添加了 Dependency 属性,其名称也在上面的注册中指定。因此,对于此构造函数,将注入 CustomerService 类型而不是默认的 CustomerServiceLoggingDecorator 类型。 MyController1 仍将使用 ICustomerService 的默认未命名注册,类型为 CustomerServiceLoggingDecorator

public MyController2([Dependency("plainService")]ICustomerService service) 

public MyController1(ICustomerService service) 

当您手动解析容器本身的类型时,也有一些方法可以做到这一点,请参阅 Resolving Objects by Using Overrides。这里的问题是您需要访问容器本身才能执行此操作,这是不推荐的。作为替代方案,您可以在容器周围创建一个包装器,然后将其注入到控制器(或其他类型)中,然后通过覆盖方式检索一个类型。同样,这有点混乱,如果可能的话我会避免它。

基于 Mark 的第二个答案,我希望用 InjectionFactory 注册 CustomerService 并且只使用没有它的接口的服务类型注册它,例如:

containter.RegisterType<CustomerService>(new InjectionFactory(
    container => new CustomerService(containter.Resolve<IGenericRepository<Customer>>())));

这将允许您像 Mark 的回答那样注册日志记录对象:

containter.RegisterType<ICutomerService, CutomerServiceLoggingDecorator>(new InjectionConstructor(
    new ResolvedParameter<CustomerService>()));

这基本上与我在需要延迟加载某些东西时使用的技术相同,因为我不希望我的对象依赖于 Lazy<IService> 并且通过将它们包装在代理中允许我只注入 IService 但通过代理延迟解决。

这也将允许您通过简单地为您的对象解析 CustomerService 而不是 ICustomerService.

对于日志记录 CustomerService:

container.Resolve<ICustomerService>()

或者对于非日志记录 CustomerService:

container.Resolve<CustomerService>()