关于c#:依赖注入与服务位置

Dependency Injection vs Service Location

我目前正在权衡DI和SL之间的优缺点。但是,我发现自己在下面的第22条中,这意味着我应该只使用SL来处理所有事情,并且只在每个类中注入一个IOC容器。

DI渔获量22:

有些依赖项,比如log4net,根本不适合DI。我称之为这些元依赖,并认为它们对于调用代码应该是不透明的。我的理由是,如果一个简单的类"d"最初是在没有日志记录的情况下实现的,然后增长为需要日志记录,那么依赖类"a"、"b"和"c"现在必须以某种方式获得这个依赖项,并将其从"a"传递到"d"(假设"a"组合"b"、"b"组合"c"等等)。我们现在做了大量的代码更改,仅仅是因为我们需要登录一个类。

因此,我们需要一种不透明的机制来获取元依赖性。需要注意的两个方面是:singleton和sl。前者有已知的局限性,主要是关于严格的作用域功能:最多,singleton将使用存储在应用程序作用域(即静态变量)中的抽象工厂。这允许一些灵活性,但并不完美。

更好的解决方案是将IOC容器注入此类类中,然后在该类中使用sl来解析容器中的这些元依赖项。

因此,第22条是:因为类现在正被注入一个IOC容器,那么为什么不使用它来解析所有其他依赖项呢?

我非常感谢你的想法:)


Because the class is now being injected with an IoC container, then why not use it to resolve all other dependencies too?

使用服务定位器模式完全破坏了依赖注入的一个主要点。依赖注入的要点是使依赖显式化。一旦通过不在构造函数中显式地设置参数来隐藏这些依赖项,就不再进行完整的依赖项注入。

这些都是名为Foo的类的构造函数(设置为约翰尼现金歌曲的主题):

错误:

1
2
3
public Foo() {
    this.bar = new Bar();
}

错误:

1
2
3
public Foo() {
    this.bar = ServiceLocator.Resolve<Bar>();
}

错误:

1
2
3
public Foo(ServiceLocator locator) {
    this.bar = locator.Resolve<Bar>();
}

正确的:

1
2
3
public Foo(Bar bar) {
    this.bar = bar;
}

只有后者才明确依赖于Bar

至于日志记录,有一个正确的方法可以做到这一点,而不会渗透到您的域代码中(不应该这样做,但如果这样做了,那么您将使用依赖注入周期)。令人惊讶的是,IOC容器可以帮助解决这个问题。从这里开始。


服务定位器是一种反模式,其原因在http://blog.ploeh.dk/2010/02/03/servicelocatorisanantpattern.aspx中有很好的描述。在日志记录方面,您可以像对待任何其他依赖一样将其视为依赖项,并通过构造函数或属性注入注入抽象。

与log4net的唯一区别是,它需要使用该服务的调用方的类型。使用ninject(或其他容器),如何找到请求服务的类型?描述如何解决此问题(它使用ninject,但适用于任何IOC容器)。

或者,您可以将日志记录看作是一个跨领域的问题,这不适合与业务逻辑代码混合使用,在这种情况下,您可以使用由许多IOC容器提供的拦截。http://msdn.microsoft.com/en-us/library/ff647107.aspx描述了使用带有Unity的拦截。


我认为这要看情况而定。有时一个更好,有时另一个更好。但我想说的是,将军,我更喜欢DI。原因不多。

  • 当依赖项以某种方式注入组件时,可以将其视为其接口的一部分。因此,组件的用户更容易提供这些依赖项,因为它们是可见的。在注入SL或静态SL的情况下,依赖关系是隐藏的,组件的使用有点困难。

  • 注入的依赖性对于单元测试更好,因为您可以简单地模拟它们。对于SL,您必须再次设置定位器+模拟依赖项。所以这是更多的工作。


  • 有时日志记录可以使用AOP实现,这样它就不会与业务逻辑混合。

    否则,选项包括:

    • 使用一个可选的依赖项(如setter属性),对于单元测试,您不会注入任何记录器。如果您在生产中运行,IOC容器将自动为您设置它。
    • 当你有一个几乎所有应用程序对象都在使用的依赖项时("logger"对象是最常见的例子),这是少数单例反模式成为良好实践的情况之一。有些人把这些"优秀的单身汉"称为一种环境背景:http://aabs.wordpress.com/2007/12/31/the-ambient-context-design-pattern-in-net/

    当然,这个上下文必须是可配置的,以便您可以使用存根/模拟进行单元测试。AmbientContext的另一个建议用法是将当前日期/时间提供程序放在那里,这样您就可以在单元测试期间将其截短,并根据需要加速时间。


    我知道这个问题有点老,我只是想给我一些建议。

    事实上,10次中有9次你真的不需要SL,应该依赖DI。但是,在某些情况下,您应该使用SL。我发现自己使用SL(或其变体)的一个领域是游戏开发。

    SL的另一个优势(在我看来)是能够绕过internal类。

    下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    internal sealed class SomeClass : ISomeClass
    {
        internal SomeClass()
        {
            // Add the service to the locator
            ServiceLocator.Instance.AddService<ISomeClass>(this);
        }

        // Maybe remove of service within finalizer or dispose method if needed.

        internal void SomeMethod()
        {
            Console.WriteLine("The user of my library doesn't know I'm doing this, let's keep it a secret");
        }
    }

    public sealed class SomeOtherClass
    {
        private ISomeClass someClass;

        public SomeOtherClass()
        {
            // Get the service and call a method
            someClass = ServiceLocator.Instance.GetService<ISomeClass>();
            someClass.SomeMethod();
        }
    }

    正如您所看到的,库的用户不知道这个方法是被调用的,因为我们没有DI,无论如何我们都不能。


    这是关于"服务定位器是一个反模式"的马克西曼。我可能错了。但我想我也应该分享我的想法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class OrderProcessor : IOrderProcessor
    {
        public void Process(Order order)
        {
            var validator = Locator.Resolve<IOrderValidator>();
            if (validator.Validate(order))
            {
                var shipper = Locator.Resolve<IOrderShipper>();
                shipper.Ship(order);
            }
        }
    }

    orderProcessor的process()方法实际上没有遵循"控制反转"原则。它还打破了方法级别的单一责任原则。为什么一个方法应该关注实例化

    对象(通过新的或任何S.L.类)它需要完成任何事情。

    与使用process()方法创建对象不同,构造函数实际上可以拥有各个对象(读依赖项)的参数,如下所示。那么,服务定位器如何能与国际奥委会有所不同呢

    容器。它也将有助于单元测试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class OrderProcessor : IOrderProcessor
    {
        public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
        {
            this.validator = validator;
            this.shipper = shipper;
        }

        public void Process(Order order)
        {

            if (this.validator.Validate(order))
            {
                shipper.Ship(order);
            }
        }
    }


    //Caller
    public static void main() //this can be a unit test code too.
    {
    var validator = Locator.Resolve<IOrderValidator>(); // similar to a IOC container
    var shipper = Locator.Resolve<IOrderShipper>();

    var orderProcessor = new OrderProcessor(validator, shipper);
    orderProcessor.Process(order);

    }


    我们已经达成了一个折衷方案:使用DI,但是将顶级依赖项捆绑到一个对象中,避免在这些依赖项发生变化时重构地狱。

    在下面的示例中,我们可以添加到"ServiceDependencies",而不必重构所有派生的依赖项。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public ServiceDependencies{
         public ILogger Logger{get; private set;}
         public ServiceDependencies(ILogger logger){
              this.Logger = logger;
         }
    }

    public abstract class BaseService{
         public ILogger Logger{get; private set;}

         public BaseService(ServiceDependencies dependencies){
              this.Logger = dependencies.Logger; //don't expose 'dependencies'
         }
    }


    public class DerivedService(ServiceDependencies dependencies,
                                  ISomeOtherDependencyOnlyUsedByThisService                       additionalDependency)
     : base(dependencies){
    //set local dependencies here.
    }

    我在Java中使用了谷歌Guice DI框架,发现它比测试更简单。例如,我需要每个应用程序(而不是类)一个单独的日志,进一步要求所有公共库代码在当前调用上下文中使用记录器。注入记录器使这成为可能。诚然,所有的库代码都需要更改:记录器被注入到构造函数中。起初,我拒绝了这种方法,因为需要进行所有的编码更改;最终我意识到这些更改有很多好处:

    • 代码变得更简单了
    • 代码变得更加健壮
    • 类的依赖关系变得明显
    • 如果有许多依赖项,这就清楚地表明类需要重构。
    • 消除了静态单体
    • 对会话或上下文对象的需求消失了
    • 多线程变得更加容易,因为DI容器可以构建为只包含一个线程,从而消除无意中的交叉污染。

    不用说,我现在是DI的忠实拥趸,除了最微不足道的应用程序外,其他所有应用程序都使用它。


    如果示例仅将log4net作为依赖项,则只需执行以下操作:

    1
    ILog log = LogManager.GetLogger(typeof(Foo));

    没有必要注入依赖项,因为log4net通过将类型(或字符串)作为参数来提供粒度日志记录。

    此外,DI与SL不相关。imho服务定位器的目的是解决可选的依赖关系。

    如果SL提供了一个ILOG接口,我将写日志DAA。


    我知道人们真的说DI是唯一好的IOC模式,但我不明白。我想卖一点SL。我将使用新的MVC核心框架向您展示我的意思。第一个DI引擎非常复杂。当人们说DI时,真正的意思是使用一些框架,比如Unity、Ninject、Autopac…这对你来说是一种沉重的负担,在那里SL可以像制造工厂级的课程一样简单。对于一个小型的快速项目来说,这是一个简单的方法,可以在不学习完整的DI框架的情况下完成IOC,虽然学习起来可能并不那么困难,但仍然如此。现在讨论一下DI可以变成的问题。我将使用MVC核心文档的报价。"ASP.NET核心是从一开始就设计来支持和利用依赖注入的。"大多数人都说关于DI"99%的代码库应该不知道您的IOC容器。"那么,如果只有1%的代码应该知道它,而不是旧的MVC支持DI,为什么他们需要从头开始设计呢?这是DI的大问题,它取决于DI。使一切"按应该做的"工作需要大量的工作。如果您查看新的动作注入,那么如果您使用[FromServices]属性,这不取决于DI。现在DI的人会说不,你应该和工厂一起去,而不是这些东西,但是你可以看到,即使是制造MVC的人也没有做对。DI的问题在过滤器中是可见的,同时看看您需要做什么才能让DI进入过滤器中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class SampleActionFilterAttribute : TypeFilterAttribute
    {
        public SampleActionFilterAttribute():base(typeof(SampleActionFilterImpl))
        {
        }

        private class SampleActionFilterImpl : IActionFilter
        {
            private readonly ILogger _logger;
            public SampleActionFilterImpl(ILoggerFactory loggerFactory)
            {
                _logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
            }

            public void OnActionExecuting(ActionExecutingContext context)
            {
                _logger.LogInformation("Business action starting...");
                // perform some business logic work

            }

            public void OnActionExecuted(ActionExecutedContext context)
            {
                // perform some business logic work
                _logger.LogInformation("Business action completed.");
            }
        }
    }

    其中,如果使用SL,则可以使用var _logger=locator.get();。然后我们来看看风景。尽管有关于DI的良好意愿,他们不得不使用SL来查看这些视图。新语法@inject StatisticsService StatsServicevar StatsService = Locator.Get();相同。DI最引人注目的部分是单元测试。但是,人们和UP所做的只是在那里测试模拟服务,而没有任何目的,或者必须连接DI引擎来进行真正的测试。我知道你可以做任何不好的事情,但是人们最终会制造一个SL定位器,即使他们不知道它是什么。没有很多人不先读就做DI。我对DI最大的问题是,类的用户必须知道类在另一个类中的内部工作才能使用它。SL可以很好地使用,它有一些优点,最重要的是它的简单性。