.NET Core处处都是DI,但是团队大部分小伙伴并不理解这个基本概念,所以有必要结合.NET Core做一个大概的介绍。
DI不是凭空想出来的,是逐渐的降低耦合度发展出来的,我们这里也尝试一下这个发展过程,大家理解就更为清晰。
1. 对象依赖
在A里用到B类的实例化构造,就可以说A依赖于B,这个会导致什么问题了?:
- A要使用B必须了解B的所有内容细节,B可能提供了n个方法,但是A实际上只需要用到其中2个,其它的和A完全无关。
- B发生任何变化都会影响到A,开发A和开发B的人可能不是一个人,B把一个A需要用到的方法参数改了,B的修改能编译通过,能继续用,但是A就跑不起来了。
- 最重要的是,如果A想把B替换成C,则A的改动会非常大。A是服务使用者,B是提供一个具体服务的,C也能提供类似服务,但是A已经严重依赖于B了,想换成C非常之困难。
一个具体的例子,B是文件存储类,A使用B来存储内容,C是网盘存储类,我已经用了B来存储资源到我们自己的服务器硬盘,现在想换成存到网盘上,对于下面的例子来说,当然简单,在实际项目中几乎就是大改动了,带来很大风险。
示例代码如下:
//B类
Class B{
public string writeContentToFile(string content){
//把content写到一个随机名字的文件path里,然后把文件名返回
return path;
}
}
Class C{
public string writeContentToPan(string content){
//把content写到一个网盘里,然后把url返回
return url;
}
}
//A类
Class A{
static void Main(string[] args)
{
B b = new B();
string path = b.writeContentToFile("哈哈");
Console.WriteLine(path);
//如果要换成C,代码都改了
C c = new C();
string url=c.writeContentToPan("哈哈");
Console.WriteLine(url);
}
}
2. 接口依赖
学过面向对象的同学马上会知道可以使用接口来解决这几个问题,还是上面的例子,如果早期实现类B的时候就定义了一个接口叫IWriter,B和C都会实现这个接口里的方法,这样从B切换到C就只需改一行了:
//IWriter接口
interface IWriter{
public string write(string content);
}
//B类
Class B : IWriter{
public string write(string content){
//写到我们自己服务器的硬盘上并返回一个url指向这个文件
return 服务器rooturl+writeContentToFile(content);
}
private string writeContentToFile(string content){
//把content写到一个随机名字的文件path里,然后把文件名返回
return path;
}
}
Class C: IWriter{
public string write(string content){
return writeContentToPan(content);
}
public string writeContentToPan(string content){
//把content写到一个网盘里,然后把url返回
return url;
}
}
//A类
Class A{
static void Main(string[] args)
{
IWriter writer = new B();
//从B改成C只需要把上面一行的B改成C就可以了
string url= writer.write("哈哈");
Console.WriteLine(url);
}
}
A对B对C的依赖变成对IWriter的依赖了,上面说的几个问题都解决了。但是目前还是得实例化B或者C,因为new只能new对象,不能new一个接口,还不能说A彻底只依赖于IWriter了。从B切换到C还是需要重新编译和发布A,能做到更少的依赖吗?能做到A在运行的时候想切换B就B,想切换C就C,不用改任何代码甚至还能支持以后切换成D吗?
3. 依赖注入(DI)
我们先不考虑DI这个概念,我们先用我们自己的方法来解决上面的问题,这里就需要用到反射,反射的概念大家可以仔细去了解一下,这个是很重要的基础概念,我们这里先简单认为反射可以实现通过加载一个类的名称字符串来运行时动态new一个对象,Java和C#都有类似的功能,我们假定这个方法叫 NewInstanceByString
,看看下面的代码示例:
IWriter b = new B();
IWriter b = NewInstanceByString("B"); //等同于上一句代码
IWriter c = new C();
IWriter c = NewInstanceByString("C"); //等同于上一句代码
有了反射我们就能解决上面的问题
我们需要增加一个配置文件config.json
IWriter、B和C没有变化
//A类
Class A{
static void Main(string[] args)
{
//如果config.json里存的B就会new一个B对象,存的C就会new一个C对象
IWriter writer = NewInstanceByString(ReadFile("config.json"));
string url= writer.write("哈哈");
Console.WriteLine(url);
}
}
大家可以看到,我想从B切换到C或者D,只需要修改config.json就可以了,源码完全不用修改,A做到只依赖于IWriter了,这就是DI实现的基本思路。其中注入的意思就是在运行时动态实例化一个对象,就像打针一样注入到这个对象的使用者A,对于A来说并不需要知道是B还是C还是D被注入了。
针对这种情况的基础上理论化这种思想,创建了一些大家达到共识的概念,创立了DI这个大家都认可的标准。这个思想是和开发语言无关的。
DI的基本概念是容器,这个容器用于注册接口和对应的实现,A从容器中根据接口来获取实现,具体的实现是那个类不需要了解,用完怎么释放也不需要管。
我们接下来看这三个阶段A的依赖变化情况(假定A需要用到2个功能,每个功能有三种类似的实现方式):
通过DI,最后功能使用者A只依赖于容器和接口,不会直接再依赖于具体的实现了。
4. 过度设计
有没有过度设计的嫌疑?小伙伴说我就用.NET Core做一个业务系统,我都不用任何接口,直接用什么就new什么不行吗?有必要理解和使用DI吗?
不管是用Java还是.Net Core,都是面向对象的语言,依赖抽象而不依赖具体实现已经成为一个基本共识,包括NetCore的框架,包括nuget上许多好用的库,也都是基于DI来设计和完成的,我们没有理由不去理解和使用它。
我的想法是需要有这种思维习惯,不是说写任何功能都需要先定义接口,使用DI,而是在写强依赖的代码的时候,停下来花一分钟下意识的想想有这里的逻辑有没有变化和扩展的可能,想想我写的功能有没有可能被别人使用,有没有必要降低耦合度。
从工作量上来说,一开始就有这种想法和习惯,这种代码的改动不会费劲和有风险。相反,如果到了项目快结束或者做完了,才碰见那种从B要切换到C的情况,就抓瞎了。
下一部分会再分析DI在.NET Core下的应用