从UML来理解依赖
1.1什么是依赖
我们先看下图
可以简单理解,一个HomeController类使用到了DBContext类,而这种关系是有偶然性,临时性,弱关系的,但是DBContext的变化会影响到HomeController
1.2显示依赖和隐式依赖
先看显示依赖代码:
显示依赖通过构造函数,很清楚的描述了HomeController类都依赖了哪些对象,这样就可以很好的管理这些依赖。而隐式依赖的缺点刚好就是显示依赖的优点。我们看下面的隐式依赖:
如果一个类有上千行代码,到处都充斥着该类型的代码,这些代码就像隐藏的病毒一样,无处不在,可以想象后续的变化和修改是多么的恐怖。
1.3依赖倒置
依赖倒置的概念其实很简单,一句话就讲完了:我们要依赖抽象,而不依赖具体实现。什么是抽象?比如接口,抽象类就是。
依赖抽象的目的是什么?封装变化!因为所有实现接口的实现都可以互相替换。
如上图所示,当数据库DapperUserRepository切换到EfUserRepository,对HomeController类可以无需任何修改,就可以平滑切换过去。反之,则更改的面就会非常大。
再看下面的代码,OrderController依赖接口IUserRepository就是依赖倒置的表现。
从单元测试来理解
也许你会说,我的变化没有那么频繁,不需要那么麻烦。那么你是否考虑过,有可能自己的代码需要进行单元测试?如果存在这种可能,那么依赖注入是你必须要做的事。
2.1控制反转
我们再看下面这个代码的问题
虽然OrderController依赖的是接口IUserRepository,满足依赖倒置原则,但是构造函数却依赖的是具体实现类UserRepository,这种做法属于硬编码,仍然无法满足未来变化带来的修改,怎么办?接下来我们来讲控制反转这个相对难以理解的概念。
先说反转,到底反转的是什么?我们知道OrderController依赖的对象UserRepository是在构造函数内的生成的。如何能够把该对象的生成交给外部去决定生成呢?可以的!这种转移对象生成的方式就是控制反转。
简而言之,反转的是控制权,即依赖对象生成的控制权。是自己决定生成还是交由别人去决定生成。
所以上面的代码,修改如下:
以上的代码才达到真正的控制反转,UserRepository对象的生成完全交由外部进行控制,交给变化去控制。
这样有什么好处呢?交给外部生成的最大好处是想要生成什么对象可以自由控制,这样还是为了将来对象生成的可替换,比如数据库访问对象的变更;单元测试的实现类替换等等。
2.2单元测试
有了上面的控制反转,我们的单元测试就方面很多了。
我们可以看到,在数据库无法连接的时候,我们可以使用MemoryUserRepository进行替换单元测试,非常方便。