【问题标题】:Dependency Injection composition root and decorator pattern依赖注入组合根和装饰器模式
【发布时间】:2016-03-16 23:05:12
【问题描述】:

在使用依赖注入时,我在装饰器模式的实现中得到了StackoverflowException。我认为这是因为我对 DI/IoC 的理解“遗漏”了一些东西。

例如,我目前有CustomerServiceCustomerServiceLoggingDecorator。这两个类都实现了ICustomerService,装饰器类所做的只是使用注入的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>();

        }

所以现在我不确定使用依赖注入实际创建装饰器的“正确”方式。我的装饰器基于 Mark Seemann 的回答 here。在他的示例中,他正在更新几个传递给类的对象。这就是我的它“工作”* sn-p 的工作方式。但是,我认为我错过了一个基本步骤。

为什么要像这样手动创建新对象?这不是否定让容器为我解决问题的意义吗?或者我应该在这种方法中改为使用contain.Resolve()(服务定位器),以使所有依赖项仍然注入?

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

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

问题:我相信我遗漏了一条关键信息,由于循环依赖关系,这导致我找到StackoverflowExceptions。如何正确实现我的装饰器类,同时仍然遵循依赖注入/控制原则和约定的反转?

第二个问题:如果我决定我只想在某些情况下应用日志装饰器怎么办?所以如果我有MyController1 我希望有一个CustomerServiceLoggingDecorator 依赖,但MyController2 只需要一个普通的CustomerService,我如何创建两个单独的注册?因为如果我这样做:

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

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

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

【问题讨论】:

标签: c# asp.net-mvc dependency-injection inversion-of-control unity-container


【解决方案1】:

序言

每当您在使用 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。无论如何,你可能不应该这样做,所以它不应该是一个问题,所以这可能是一个更好的选择。

【讨论】:

  • 这很有帮助,谢谢。我会更多地研究 Pure DI。我选择了使用魔术字符串的第一种方法,因为我相信这将允许我选择哪些控制器将具有日志记录(通过传递装饰器或实际服务)。但是,我实际上会在哪里选择控制器中的哪一个?这是组合根(Global.asax)发挥作用的地方吗?因为目前装饰器现在总是被传递给使用 ICustomerService 的控制器。
  • @user9993 你可以请求一个命名组件。如果您要求一个名为“custSvc”的ICustomerService 组件,您将获得CustomerService 和上面显示的第一个替代方案。如果你要求一个未命名的组件,你会得到装饰器。
  • 我明白了,这是有道理的。请求命名组件的最佳位置在哪里? Unity内发RegisterTypes方法?
  • @user9993 你应该遵循Register Resolve Release 模式;你应该只从你的Composition Root解析。
  • 我打算将赏金奖励给这个答案,我一定是脑残了,因为不知何故我最终把它交给了其他答案之一。对不起!不过,我已将其标记为已接受的答案。
【解决方案2】:

问题:我认为我遗漏了一条关键信息,由于循环依赖,这导致我出现 StackoverflowExceptions。如何正确实现我的装饰器类,同时仍然遵循依赖注入/控制原则和约定的反转?

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

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

这允许您指定如何按类型解析参数。您也可以按名称执行此操作,但按类型执行是一种更简洁的实现,并且允许在编译期间进行更好的检查,因为不会捕获字符串中的更改或错误输入。 请注意,此代码部分与 Mark Seemann 提供的代码之间的唯一细微差别是对 InjectionConstructor 的拼写进行了更正。这部分我不再详述,因为没有什么要补充的,Mark Seemann 已经解释过了。


第二个问题:如果我决定我只想在某些情况下应用日志装饰器怎么办?所以如果我有 MyController1 我希望有一个 CustomerServiceLoggingDecorator 依赖项,但 MyController2 只需要一个普通的 CustomerService,我如何创建两个单独的注册?

您可以使用上面指定的方式使用 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。这里的问题是您需要访问容器本身才能执行此操作,这是不推荐的。作为替代方案,您可以在容器周围创建一个包装器,然后将其注入到控制器(或其他类型)中,然后通过覆盖以这种方式检索类型。同样,这有点混乱,如果可能的话我会避免它。

【讨论】:

    【解决方案3】:

    在马克的第二个答案的基础上,我希望用InjectionFactory 注册CustomerService,并且只使用没有它的接口的服务类型注册它:

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

    这将允许您像马克的回答一样注册日志对象,例如:

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

    这与我在需要延迟加载某些内容时使用的技术基本相同,因为我不希望我的对象依赖于Lazy&lt;IService&gt;,并且通过将它们包装在代理中允许我只注入 IService 但有它通过代理延迟解析。

    这也将允许您通过简单地为您的对象解析 CustomerService 而不是 ICustomerService 来选择日志对象或普通对象的注入位置,而不需要 magic 字符串。

    对于日志记录CustomerService

    container.Resolve&lt;ICustomerService&gt;()

    或者对于非日志CustomerService

    container.Resolve&lt;CustomerService&gt;()

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-05-02
      • 1970-01-01
      • 2016-02-08
      • 2012-03-20
      • 2016-10-20
      • 1970-01-01
      • 2015-09-05
      相关资源
      最近更新 更多