繁体   English   中英

依赖注入与服务位置

[英]Dependency Injection vs Service Location

我目前正在权衡DI和SL之间的优缺点。 但是,我发现自己处于以下问题22中,这意味着我应该只使用SL作为一切,并且只在每个类中注入一个IoC容器。

DI Catch 22:

一些依赖项,如Log4Net,根本不适合DI。 我称之为元依赖关系并认为它们对调用代码应该是不透明的。 我的理由是,如果一个简单的类'D'最初是在没有记录的情况下实现的,然后增长到需要记录,那么依赖类'A','B'和'C'现在必须以某种方式获得这种依赖并将其从'A'到'D'(假设'A'组成'B','B'组成'C',依此类推)。 我们现在已经进行了重要的代码更改,因为我们需要登录一个类。

因此,我们需要一种不透明的机制来获取元依赖性。 我想到了两个:Singleton和SL。 前者具有已知的局限性,主要是关于刚性范围的能力:最好的是Singleton将使用存储在应用程序范围内的抽象工厂(即在静态变量中)。 这允许一些灵活性,但并不完美。

更好的解决方案是将IoC容器注入此类,然后使用该类中的SL从容器中解析这些元依赖关系。

因此,捕获22:因为类现在正在注入IoC容器,那么为什么不使用它来解析所有其他依赖项呢?

我非常感谢你的想法:)

因为该类现在正在注入IoC容器,那么为什么不使用它来解析所有其他依赖项呢?

使用服务定位器模式完全打败了依赖注入的一个主要点。 依赖注入的关键是使依赖关系显式化。 一旦你通过不在构造函数中使它们成为显式参数来隐藏这些依赖项,就不再需要完全成熟的依赖注入了。

这些都是名为Foo的类的构造函数(设置为Johnny Cash歌曲的主题):

错误:

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

错误:

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

错误:

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

对:

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

只有后者才能明确依赖Bar

对于日志记录,有一种正确的方法可以实现它,而不会渗透到您的域代码中(它不应该,但如果确实如此,则使用依赖注入时段)。 令人惊讶的是,IoC容器可以帮助解决这个问题。 这里开始。

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

与log4net唯一的区别是它需要使用该服务的调用者类型。 使用Ninject(或其他一些容器)如何找出请求服务的类型? 描述了如何解决这个问题(它使用Ninject,但适用于任何IoC容器)。

或者,您可以将日志记录视为交叉问题,这不适合与业务逻辑代码混合使用,在这种情况下,您可以使用由许多IoC容器提供的拦截。 http://msdn.microsoft.com/en-us/library/ff647107.aspx描述了使用Unity进行拦截。

我的意见是,这取决于。 有时一个更好,有时另一个。 但我会说通常我更喜欢DI。 原因很少。

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

  2. 注入的依赖对于单元测试更好,因为您可以简单地模拟它们。 在SL的情况下,您必须再次设置Locator + mock依赖项。 所以这是更多的工作。

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

否则,选项是:

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

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

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

  • 代码变得更简单了
  • 代码变得更加健壮
  • 类的依赖性变得明显
  • 如果存在许多依赖关系,则清楚地表明类需要重构
  • 静态单身人士被淘汰出局
  • 对会话或上下文对象的需求消失了
  • 多线程变得更容易,因为DI容器可以构建为仅包含一个螺纹,从而消除了无意的交叉污染

毋庸置疑,我现在是DI的忠实粉丝,除了最琐碎的应用程序之外,它还可以用于所有应用程序。

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

在下面的示例中,我们可以添加到“ServiceDependencies”,而无需重构所有派生的依赖项。

例:

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.
}

这是关于Mark Seeman的“服务定位器是一种反模式”。 我可能在这里错了。 但我只是觉得我也应该分享我的想法。

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()方法实际上并不遵循“控制反转”原则。 它还打破了方法级别的单一责任原则。 为什么一个方法应该关注实例化

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

而不是使用Process()方法创建对象,构造函数实际上可以具有相应对象的参数(读取依赖项),如下所示。 那么服务定位器如何与IOC有任何不同

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

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);

}

我知道这个问题有点老了,我以为我会给出我的意见。

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

SL(在我看来)的另一个优点是能够传递internal类。

以下是一个例子:

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,不是我们无论如何都不能。

如果该示例仅将log4net作为依赖项,那么您只需要执行以下操作:

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

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

此外,DI与SL无关。 恕我直言,ServiceLocator的目的是解决可选的依赖项。

例如:如果SL提供ILog接口,我将编写日志记录daa。

我知道人们真的说DI是唯一的好IOC模式,但我不明白。 我会尝试卖掉SL。 我将使用新的MVC Core框架向您展示我的意思。 第一台DI发动机非常复杂。 当人们说DI时,人们真正的意思是使用像Unity,Ninject,Autofac这样的框架......为你做所有繁重的工作,SL可以像制造工厂一样简单。 对于一个小型的快速项目,这是一个简单的方法来做IOC而不学习正确的DI的整个框架,他们可能不是那么难学,但仍然。 现在到了DI可以成为的问题。 我将使用MVC Core docs的引用。 “ASP.NET Core是从头开始设计的,用于支持和利用依赖注入。” 大多数人都说DI“99%的代码库应该不了解你的IoC容器。” 那么为什么他们需要从头开始设计,如果只有1%的代码应该知道它,那么旧的MVC不支持DI吗? 那么这是DI的重大问题,它取决于DI。 让一切正常“因为它应该完成”需要做很多工作。 如果您使用[FromServices]属性,如果查看新的Action Injection,则不依赖于DI。 现在DI人们会说不,你不应该选择工厂而不是这个东西,但正如你所看到的那样,即使是制造MVC的人也没有做到这一点。 在过滤器中可以看到DI的问题,看看在过滤器中获取DI需要做些什么

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<StatisticsService>(); DI中最广告的部分是单元测试。 但是人们和正在做的只是测试模拟服务没有任何目的或者必须在那里连接DI引擎来进行真正的测试。 而且我知道你可以做任何不好的事情,但即使他们不知道它是什么,人们也会最终制作SL定位器。 没有很多人在没有先阅读的情况下制作DI。 DI的最大问题是类的用户必须知道该类的内部工作方式才能使用它。
SL可以很好地使用,并且具有一些优点,最重要的是它的简单性。

对于DI,您是否需要对注入型组件进行硬引用? 我没有看到有人在谈论这个问题。 对于SL,我可以告诉我的解析器在需要时从config.json或类似地动态加载我的类型。 此外,如果您的程序集包含数千种类型及其继承,您是否需要对服务集合提供程序进行数千次级联调用才能注册它们? 这就是我看到很多话题的地方。 大多数人都在谈论DI的好处及其一般情况,当谈到如何在.net中实现它时,他们提出了一种扩展方法,用于添加对硬链接类型程序集的引用。 这对我来说并不是很脱钩。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM