DI 的主要好处不是单元测试(尽管这肯定是一个好处)。主要好处是松耦合。 “可测试”的应用程序不一定是松耦合的。
但是,松耦合带来的不仅仅是可测试性。
松散耦合的好处
- 后期绑定(服务可以与其他服务交换,通常无需更改现有代码)
- 可扩展性(代码可以扩展,通常无需更改现有代码)
- 并行开发(定义了多个开发人员可以遵守的抽象合同)
- 可维护性(职责明确的类更易于维护)
- 可测试性(类更容易测试)。
恕我直言,当将 DI 与软件模式相结合时,可扩展性绝对是主要优势。考虑以下类型:
public interface IWriter
{
void WriteSomething();
}
public interface ISomeService
{
void Write();
}
您可以使用Decorator Pattern 扩展服务:
public class NullWriter : IWriter
{
public void WriteSomething()
{
// Do nothing - this is a "null object pattern".
}
}
public class HelloWriter : IWriter
{
public readonly IWriter innerWriter;
public HelloWriter(IWriter innerWriter)
{
if (innerWriter == null)
throw new ArgumentNullException("innerWriter");
this.innerWriter = innerWriter;
}
public void WriteSomething()
{
this.innerWriter.WriteSomething();
Console.WriteLine("Hello.");
}
}
public class GoodbyeWriter : IWriter
{
public readonly IWriter innerWriter;
public GoodbyeWriter(IWriter innerWriter)
{
if (innerWriter == null)
throw new ArgumentNullException("innerWriter");
this.innerWriter = innerWriter;
}
public void WriteSomething()
{
this.innerWriter.WriteSomething();
Console.WriteLine("Goodbye.");
}
}
public class SomeService : ISomeService
{
private readonly IWriter writer;
public SomeService(IWriter writer)
{
if (writer == null)
throw new ArgumentNullException("writer");
}
public void Write()
{
this.writer.WriteSomething();
}
}
上面的内容会像这样连接起来:
// Composition Root
var nullWriter = new NullWriter();
var goodbyeWriter = new GoodbyeWriter(nullWriter);
var helloWriter = new HelloWriter(goodbyeWriter);
var service = new SomeService(helloWriter);
// End Composition Root
// Execute
service.Write();
//Writes:
//Hello.
//Goodbye.
现在,已经设置了场景,您可以扩展 SomeService 所做的事情而无需更改任何现有类型。应用程序中唯一需要更改的部分是composition root。
public class HowAreYouWriter : IWriter
{
public readonly IWriter innerWriter;
public HowAreYouWriter(IWriter innerWriter)
{
if (innerWriter == null)
throw new ArgumentNullException("innerWriter");
this.innerWriter = innerWriter;
}
public void WriteSomething()
{
this.innerWriter.WriteSomething();
Console.WriteLine("How are you?");
}
}
// Composition Root
var nullWriter = new NullWriter();
var goodbyeWriter = new GoodbyeWriter(nullWriter);
var howAreYouWriter = new HowAreYouWriter(goodbyeWriter);
var helloWriter = new HelloWriter(howAreYouWriter);
var service = new SomeService(helloWriter);
// End Composition Root
// Execute
service.Write();
//Writes:
//Hello.
//How are you?
//Goodbye.
约定优于配置
DI 的另一个(经常被忽视的)好处是Convention over Configuration。当将构造函数注入与 DI 容器结合使用时,它们中的许多都提供了将 ISomeService 自动映射到 SomeService 的能力。一些容器(如 StructureMap)还可以构建您自己的约定。
好处并不明显,因为直到您使用约定注册数十种类型时,它才真正开始得到回报。但是,如果使用它们,您可以大大减少编写应用程序所需的代码量。
N 层应用
对于单个应用程序,通常有一个composition root 尽可能靠近应用程序的入口点。在 MVC 中,这将在 HttpApplication.Start 方法中。
但是,这可能会有所不同,具体取决于您是否将应用程序设计的层视为DI Friendly Libraries 或DI Friendly Frameworks,以及您是否将应用程序的一部分视为您添加到它之后的“插件”已构建(基本上是制作可以加载动态依赖项的组合根)。
基本上有 3 种常用的方法来解决这个问题:
- 公开所有类型并将它们组合在包含您的表示层的同一个项目中。
- 在每一层都放一个组合根,让每一层的公共API在内部使用DI。然后使用主项目编写该层的公共 API。通常,您还需要在从主
Compose 方法调用的每一层上创建一个公共Compose 方法。
- 制作一个单独的合成根项目以将所有部分合成在一起。
我什至看到有些人建议将所有“层”放入一个项目中,如果您不打算单独使用这些部分,那么没有太大的缺点。 NuGet Gallery 就是以这种方式构建的此类项目之一。
恕我直言,公开所有内容并将组合根放在主应用程序中通常是单个多层应用程序的最佳选择,该应用程序的部分不打算与另一个应用程序一起使用。
最后一句话
如果您对学习 DI 很认真,请阅读 Mark Seemann 的《Dependency Injection in .NET》一书。您可能不认为 DI 是一个足够大的开发领域,可以单独研究,但本书确实提供了许多超越 DI 的好处,例如 SOLID 原则。