从服务定位器到依赖注入

use*_*375 1 c# dependency-injection service-locator simple-injector

我准备了一些示例应用程序,基于这些应用程序,我想讨论转移到依赖注入而不是服务定位器的问题。我在DI领域还很新,所以请耐心等待。示例应用使用Simple Injector作为DI库编写。Bootstrapper中的注册按预期工作。每当我需要IMessageBox接口和新实例ComputationCores时,我都会使用它。

我阅读了一些有关DI的文章,因此我知道应该有一些Composition根目录以及它应该如何工作。但是我发现只是非常基本的示例,没有实词的复杂性。

样例代码:

public class DependencyResolver
{

    public static Func<Type, object> ResolveMe;

    public static T GetInstance<T>() where T : class
    {
        return (T)ResolveMe(typeof (T));
    }
}

public interface IMessageBox
{
    void ShowMessage(string message);
}

public class StandardMessageBox : IMessageBox
{
    public StandardMessageBox()
    {
        Console.WriteLine("StandardMessageBox constructor called...");
    }

    ~StandardMessageBox()
    {
        Console.WriteLine("StandardMessageBox destructor called...");
    }

    public void ShowMessage(string message)
    {
        Console.WriteLine(message);
    }
}

public interface IComputationCoreAlpha
{
    int RunComputation(int myParam);
}

public class SyncComputationCoreAlpha : IComputationCoreAlpha
{
    public SyncComputationCoreAlpha()
    {
        Console.WriteLine("SyncComputationCoreAlpha constructor called...");
    }

    ~SyncComputationCoreAlpha()
    {
        Console.WriteLine("SyncComputationCoreAlpha destructor called...");
    }

    public int RunComputation(int myParam)
    {
        return myParam * myParam;
    }
}

public class AsyncComputationCoreAlpha : IComputationCoreAlpha
{
    public AsyncComputationCoreAlpha()
    {
        Console.WriteLine("AsyncComputationCoreAlpha constructor called...");
    }

    ~AsyncComputationCoreAlpha()
    {
        Console.WriteLine("AsyncComputationCoreAlpha destructor called...");
    }


    public int RunComputation(int myParam)
    {
        return myParam * myParam;
    }
}

public interface IComputationCoreBeta
{
    int RunComputation(int myParam);
}

public class SyncComputationCoreBeta : IComputationCoreBeta
{
    public SyncComputationCoreBeta()
    {
        Console.WriteLine("SyncComputationCoreBeta constructor called...");
    }

    ~SyncComputationCoreBeta()
    {
        Console.WriteLine("SyncComputationCoreBeta destructor called...");
    }


    public int RunComputation(int myParam)
    {
        return myParam * myParam;
    }
}

public class AsyncComputationCoreBeta : IComputationCoreBeta
{
    public AsyncComputationCoreBeta()
    {
        Console.WriteLine("AsyncComputationCoreBeta constructor called...");
    }

    ~AsyncComputationCoreBeta()
    {
        Console.WriteLine("AsyncComputationCoreBeta destructor called...");
    }

    public int RunComputation(int myParam)
    {
        return myParam * myParam;
    }
}

public interface IProjectSubPart
{
    int DoCalculations(int myParam);
}

public class ProjectSubPart1 : IProjectSubPart
{
    public int DoCalculations(int myParam)
    {
        var messageBoxService = DependencyResolver.GetInstance<IMessageBox>();
        messageBoxService.ShowMessage("Hardly working 1...");
        var ccA = DependencyResolver.GetInstance<IComputationCoreAlpha>();
        var ccB = DependencyResolver.GetInstance<IComputationCoreAlpha>();

        return ccA.RunComputation(myParam) + ccB.RunComputation(myParam + 1);
    }
}

public class ProjectSubPart2 : IProjectSubPart
{
    public int DoCalculations(int myParam)
    {
        var messageBoxService = DependencyResolver.GetInstance<IMessageBox>();
        messageBoxService.ShowMessage("Hardly working 2...");
        var ccA = DependencyResolver.GetInstance<IComputationCoreAlpha>();

        return ccA.RunComputation(myParam * 3);
    }
}

public class ProjectSubPartN : IProjectSubPart
{
    public int DoCalculations(int myParam)
    {
        var messageBoxService = DependencyResolver.GetInstance<IMessageBox>();
        messageBoxService.ShowMessage("Hardly working N...");
        return -3;
    }
}

public class ManhattanProject
{
    public void RunProject()
    {
        var messageBoxService = DependencyResolver.GetInstance<IMessageBox>();
        messageBoxService.ShowMessage("Project started...");
        var subPart1 = new ProjectSubPart1();
        var subPart2 = new ProjectSubPart2();
        var subPartN = new ProjectSubPartN();

        var result = subPart1.DoCalculations(1) + subPart2.DoCalculations(2) + subPartN.DoCalculations(3);

        messageBoxService.ShowMessage(string.Format("Project finished with magic result {0}", result));
    }
}

public class Sample
{
    public void Run()
    {
        BootStrapper();

        var mp = DependencyResolver.GetInstance<ManhattanProject>();
        mp.RunProject();

    }

    private void BootStrapper()
    {
        var container = new Container();
        container.RegisterSingle<IMessageBox, StandardMessageBox>();
        container.Register<IComputationCoreAlpha, SyncComputationCoreAlpha>();
        container.Register<IComputationCoreBeta, AsyncComputationCoreBeta>();

        DependencyResolver.ResolveMe = container.GetInstance;
    }

}
Run Code Online (Sandbox Code Playgroud)

在DI中,只允许在“合成”根目录中调用Container.GetInstance(resolve方法),而无处可去。大多数依赖项应注入构造函数中。

问题1:如果我要改用DI,我认为ManhattanProject的构造函数应如下所示:ManhattanProject(IMessageBox mb,IComputationCoreAlpha cca,IComputationCoreBeta ccb)。但这将导致每个mb,cca和ccb有一个实例。并非每次都有新的cca实例,ccb都可以满足我的要求。

Q1a:我想这可以通过某种抽象工厂来解决,例如cca,ccb,它可以为每个请求提供新实例。但是,那么-BootStrapper的目的是什么?

问题2:ManhattanProject可以由更多使用不同coputationCores(例如42)的ProjectSubParts组成。因此,以这种方式使用构造函数注入(以提供Computation内核)是完全不合适的,应该使用某种外观。由于Facade在构造函数中的args数量也应该有限,所以我最终会遇到许多嵌套的Facade。我想这是错误的。

Q3:我正在使用ProjectSubParts,它允许我做一些工作。所有继承自IProjectSubPart接口。如果我想为不同的ProjectSubParts注入不同的实现,我该怎么做?是否应该为每个ProjectSubpart创建新接口,以允许DI容器解析使用哪种实现?

Q4:基于提供的示例(和服务定位器模式),对我来说创建IComputationCoreAlpha实例非常容易,该实例可以在每次需要时通过调用DependencyResolver.GetInstance在内部创建新的干净内部对象。而且,我将完全控制它们,在使用完它们后,可以致电Dispose。如果在DI概念中将在CompositionRoot中创建整个图形,那么这种用法怎么可能?

谢谢

Ste*_*ven 5

问题1:如果我要改用DI,我认为ManhattanProject的构造函数应如下所示:ManhattanProject(IMessageBox mb,IComputationCoreAlpha cca,IComputationCoreBeta ccb)。

类应仅取决于直接需要的服务。因此,ManhattanProject不应依赖任何计算核心,而应仅依赖IProjectSubPart抽象。

Q1a:我想这可以通过某种抽象工厂来解决,例如cca,ccb,它可以为每个请求提供新实例。但是,那么-BootStrapper的目的是什么?

引导程序/组合根的目的是建立对象图。如果创建工厂抽象,则需要在某个地方实现它。这个“某处”是您合成的根源。工厂实现应在您的合成根目录内部。

除了使用工厂外,更好的方法是注入IEnumerable<IProjectSubPart>ManhattanProject在这种情况下,您的意愿如下:

public class ManhattanProject
{
    private readonly IMessageBox messageBoxService;
    private readonly IEnumerable<IProjectSubPart> parts;

    public ManhattanProject(IMessageBox messageBoxService, 
        IEnumerable<IProjectSubPart> parts) {
        this.messageBoxService = messageBoxService;
        this.parts = parts;
    }

    public void RunProject() {
        messageBoxService.ShowMessage("Project started...");

        var calculationResults =
            from pair in parts.Select((part, index) => new { part, value = index + 1 })
            select pair.part.DoCalculations(pair.value);

        var result = calculationResults.Sum();

        messageBoxService.ShowMessage(
            string.Format("Project finished with magic result {0}", result));
    }
}
Run Code Online (Sandbox Code Playgroud)

当您依赖时,IEnumerable<IProjectSubPart>可以防止ManhattanProject每次将新的IProjectSubPart实现添加到系统时都更改。您可以在Simple Injector中进行如下注册:

// Simple Injector v3.x
container.RegisterSingleton<IMessageBox, StandardMessageBox>();
container.Register<ManhattanProject>();
container.RegisterCollection<IProjectSubPart>(new[] {
    typeof(ProjectSubPart1),
    typeof(ProjectSubPart2),
    typeof(ProjectSubPartN)
});

// Simple Injector v2.x
container.RegisterSingle<IMessageBox, StandardMessageBox>();
container.Register<ManhattanProject>();
container.RegisterAll<IProjectSubPart>(new[] {
    typeof(ProjectSubPart1),
    typeof(ProjectSubPart2),
    typeof(ProjectSubPartN)
});
Run Code Online (Sandbox Code Playgroud)

通常,您甚至可以屏蔽应用程序的其他部分,而不必知道某个抽象有多种实现,但是在您的情况下,隐藏这种实现似乎是不可能的,因为ManhattanProject(当前)负责提供每个都有不同的价值IProjectSubPart。但是,如果可能的话,正确的解决方案是直接ManhattanProject依赖而IProjectSubPart不是依赖,IEnumerable<IProjectSubPart>而让复合根注入一个复合实现,该实现将包装此处IEnumerable<IProjectSubPart>所述。

可以将相同的模式应用于所有IProjectSubPart实现。例如:

public class ProjectSubPart1 : IProjectSubPart
{
    private readonly IMessageBox messageBoxService;
    private readonly IEnumerable<IComputationCoreAlpha> computers;

    public ProjectSubPart1(IMessageBox messageBoxService,
        IEnumerable<IComputationCoreAlpha> computers) {
        this.messageBoxService = messageBoxService;
        this.computers = computers;
    }

    public int DoCalculations(int myParam) {
        messageBoxService.ShowMessage("Hardly working 1...");

        var calculationResults =
            from pair in computers.Select((computer, index) => new { computer, index })
            select pair.computer.RunComputation(myParam + pair.index);

        return calculationResults.Sum();
    }
}
Run Code Online (Sandbox Code Playgroud)

这些IComputationCoreAlpha实现可以注册为集合,如下所示:

container.RegisterCollection<IComputationCoreAlpha>(new[] {
    typeof(SyncComputationCoreAlpha),
    typeof(AsyncComputationCoreAlpha)
});
Run Code Online (Sandbox Code Playgroud)

Q2:由于在构造函数中facade的args数量也应该有限,所以我最终会遇到很多嵌套的facade。

很难说任何有用的东西。我给定的LINQ查询实现可能不适用于您的情况,但是您的示例过于广泛,无法对此进行非常具体的说明。最后,您可能需要自定义抽象,但是我不确定。

Q3:我正在使用ProjectSubParts,它允许我做一些工作。所有继承自IProjectSubPart接口。如果我想为不同的ProjectSubParts注入不同的实现,我该怎么做?是否应该为每个ProjectSubpart创建新接口,以允许DI容器解析使用哪种实现?

这在很大程度上取决于您的设计。为此,我们应该看一下Liskov替换原理,该原理基本上说,给定抽象的任何子类型都应以与抽象兼容的方式运行。因此,在您的情况下,如果某个类期望某个IProjectSubPart实现,而在另一个实现中无法正常运行,则意味着您违反了Liskov替换原理。这意味着即使这些实现可能具有确切的方法签名,它们的行为也不相同。在这种情况下,您应该将它们拆分为多个接口。

但是,如果使用者仍然可以正常运行,并且更改实现只是一些方便,那么可以让他们具有相同的抽象是可以的。很好的例子是ILogger带有FileLoggerMailLogger实现的抽象。在系统的某些部分,您可能认为通过邮件发送消息很重要。对于依赖ILogger的类而言,无论消息是否写入文件,通过邮件发送还是完全不发送消息,它的功能都相同。

是否要违反LSK取决于您自己确定。

Q4:基于提供的示例(和服务定位器模式),对我来说创建IComputationCoreAlpha实例非常容易,该实例可以在每次需要时通过调用DependencyResolver.GetInstance在内部创建新的干净内部对象。而且,我将完全控制它们,在使用完它们后,可以致电Dispose。如果在DI概念中将在CompositionRoot中创建整个图形,那么这种用法怎么可能?

我要说,DI实际上使这项工作变得容易。例如,让我们尝试使用服务定位器实现您想要的:

public class LazyComputationCoreAlphaProxy : IComputationCoreAlpha
{
    public int RunComputation(int myParam) {    
        var heavyWeight = DependencyResolver.GetInstance<IComputationCoreAlpha>();
        return heavyWeight.RunComputation(myParam);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个代理类,可以在RunComputation调用时延迟创建实际实例。但这实际上给我们带来了一个问题。如果我们看看消费者将如何使用它,这将变得很清楚:

public int DoCalculations(int myParam) {
    var ccA = DependencyResolver.GetInstance<IComputationCoreAlpha>();
    return ccA.RunComputation(myParam);
}
Run Code Online (Sandbox Code Playgroud)

在这里,该DoCalculations方法IComputationCoreAlpha从服务定位器解析。这将返回LazyComputationCoreAlphaProxy实例(因为我们已经在定位器中注册了该实例)。解决后,我们将对其进行调用RunComputation。但RunComputation我们内部IComputationCoreAlpha再次解决。我们想解决IComputationCoreAlpha,因为否则我们的LazyComputationCoreAlphaProxy需求将直接依赖于不同的实现,但是这将导致违反依赖倒置原则,并且可能会导致我们有许多不同LazyComputationCoreAlphaProxy的。每个实现一个。

但是,如果我们尝试在IComputationCoreAlpha此处解决,定位器将LazyComputationCoreAlphaProxy再次返回给我们,最终将导致堆栈溢出异常。

现在让我们看一下依赖注入的外观:

public class LazyComputationCoreAlphaProxy : IComputationCoreAlpha
{
    private readonly Func<IComputationCoreAlpha> factory;

    public LazyComputationCoreAlphaProxy(Func<IComputationCoreAlpha> factory) {
        this.factory = factory;
    }

    public int RunComputation(int myParam) {    
        var heavyWeight = this.factory.Invoke();
        return heavyWeight.RunComputation(myParam);
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里,我们将Func工厂注入LazyComputationCoreAlphaProxys的构造函数。这使代理可以忽略其创建的实际类型,同时仍然允许与以前相同的惰性行为。现在,我们将重新构建对象图的该部分的职责委托给了我们的合成根。我们可以按以下方式手动进行连接:

LazyComputationCoreAlphaProxy(() => new SyncComputationCoreAlpha())
Run Code Online (Sandbox Code Playgroud)

或者,我们可以使用Simple Injector的装饰器工具为我们完成此操作:

container.RegisterCollection<IComputationCoreAlpha>(new[] {
    typeof(SyncComputationCoreAlpha),
    typeof(AsyncComputationCoreAlpha)
});

container.RegisterDecorator(
    typeof(IComputationCoreAlpha), 
    typeof(LazyComputationCoreAlphaProxy));
Run Code Online (Sandbox Code Playgroud)

随着RegisterDecorator注册,简单的喷油器会自动换任何IComputationCoreAlpha一个实施LazyComputationCoreAlphaProxy装饰。开箱即用,Simple Injector会了解装饰器内的Func工厂代表,并将确保注入创建了装饰对象的工厂。

但是,由于我们现在正在讨论装饰器。装饰器的依赖注入为我们提供了更多改进代码的可能性。例如,IProjectSubPart外观上的许多代码都相似。它们都有相同的消息框日志记录代码:

public class ProjectSubPart1 : IProjectSubPart
{
    private readonly IMessageBox messageBoxService;
    private readonly IEnumerable<IComputationCoreAlpha> computers;

    public ProjectSubPart1(IMessageBox messageBoxService,
        IEnumerable<IComputationCoreAlpha> computers) {
        this.messageBoxService = messageBoxService;
        this.computers = computers;
    }

    public int DoCalculations(int myParam) {
        messageBoxService.ShowMessage("Hardly working 1...");

        // part specific calculation
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您有许多不同之处IProjectSubPart,那么这是很多重复的代码,它们不仅使实际的实现复杂化,而且还需要维护。什么可以将基础结构问题(或跨领域问题)从这些类中移出,并仅实现一次:在装饰器中:

public class MessageBoxLoggingProjectSubPart : IProjectSubPart
{
    private readonly IMessageBox messageBoxService;
    private readonly IProjectSubPart decoratee;

    public MessageBoxLoggingProjectSubPart(IMessageBox messageBoxService,
        IProjectSubPart decoratee) {
        this.messageBoxService = messageBoxService;
        this.decoratee = decoratee;
    }

    public int DoCalculations(int myParam) {
        messageBoxService.ShowMessage("Hardly working 1...");

        return this.decoratee.DoCalculations(myParam);
    }
}
Run Code Online (Sandbox Code Playgroud)

使用此装饰器,您可以将零件简化为以下内容:

public class MessageBoxLoggingProjectSubPart : IProjectSubPart
{
    private readonly IMessageBox messageBoxService;
    private readonly IProjectSubPart decoratee;

    public MessageBoxLoggingProjectSubPart(IMessageBox messageBoxService,
        IProjectSubPart decoratee) {
        this.messageBoxService = messageBoxService;
        this.decoratee = decoratee;
    }

    public int DoCalculations(int myParam) {
        messageBoxService.ShowMessage("Hardly working 1...");

        return this.decoratee.DoCalculations(myParam);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,ProjectSubPart1不再需要依赖IMessageBox。这将清理实现(不要忘记您拥有的其他42个实现)。同样,如果我们将手动创建此类零件,则将执行以下操作:

public class ProjectSubPart1 : IProjectSubPart
{
    private readonly IEnumerable<IComputationCoreAlpha> computers;

    public ProjectSubPart1(IEnumerable<IComputationCoreAlpha> computers) {
        this.computers = computers;
    }

    public int DoCalculations(int myParam) {
        var calculationResults =
            from pair in computers.Select((computer, index) => new { computer, index })
            select pair.computer.RunComputation(myParam + pair.index);

        return calculationResults.Sum();
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,使用Simple Injector,这变得更加容易:

new MessageBoxLoggingProjectSubPart(new ProjectSubPart1(computers));
Run Code Online (Sandbox Code Playgroud)

现在,只要您想更改记录方式,只需更改即可MessageBoxLoggingProjectSubPart。例如,当您想在操作完成后登录时,或者万一抛出异常。这样可以避免您必须对整个应用程序进行彻底的更改(这就是“ 打开/关闭原则”的全部含义)。

对这么长的帖子我很抱歉。这是一些注入的土豆:

在此处输入图片说明