依赖注入(DI Dependency Injection)
看了网上很多文章,包括知乎,感觉解释得不够深刻,举了很多例子,但是容易把同学们搞得更晕,于是想尝试再次解释下依赖注入
什么是依赖注入
首先我们可以从字面上思考一下什么是依赖?什么是注入?
依赖:
举个实际例子(只是例子,不要关注参数绑定等东西)
public String getUserName(String uid){
Logger log = new Logger();
Databse database = new Database();
String sql = "select username from users where uid=?";
log.debug(sql);
return database.query(sql,uid);
}
这是个很简单的例子,一个方法,里面需要打log,查数据库,显然做这俩事情,依赖log和database这两个实例,这就是依赖,你的方法中需要用到其他对象就可以称之为“依赖”,或者也可以简单理解为看到new就是有依赖
注入
注入是个动词,其实依赖注入这个句子缺少了宾语,就是把依赖注入到哪里?还是刚才的例子,这个函数看起来依赖log和database,那是不是说依赖注入就是把这俩对象注入到需要用的地方呢?表面上看是的,但是一般从实现上来说都是把这种依赖关系放到一个统一的地方管理,这个地方叫做容器(Container),所以依赖注入,确切说是把依赖注入到容器中。这个容器其实就是维护了类和实例的依赖(使用)关系,本质上就是个Map
依赖注入后:
private Database database;
private Logger log;
public String getUserName(String uid){
String sql = "select username from users where uid=?";
log.debug(sql);
return database.query(sql,uid);
}
同学们可能会发现,这个代码咋看起来有问题呢,没实例化呢就调用这不空指针异常了吗,对这就是依赖注入框架要解决的问题,这里之所以没加上spring的@Autowired注解是因为本文并非针对JAVA技术栈,也不是讲spring,我们可以思考下PHP,Python,Go等语言中,要想让这个程序好使,应该怎么做?怎么能把database和log的new的过程不在这个类中实现呢?不过这不是本文的重点,感兴趣的同学可以自行百度。
这里小结下:
1、看到new xxx就可以认为是依赖xxx
2、依赖注入并不是不实例化,只是在一个地方(容器)单独管理依赖,由容器去创建和管理实例。
为什么要依赖注入
一切都是为了更好的变化
为什么要依赖注入?直接new不香吗?
大家都应该知道,接口和抽象类是不能被实例化的,反过来说,能被实例化(new)的就一定是个实现类。
那么在你的业务逻辑中,直接new就意味着你在使用一个具体的实现类,对吧
然而我们的需求是经常变化的,也就意味着实现会经常变化,如果依赖具体的实现,意味着难以更换
ps:不过可能有同学说,不难啊,批量替换啊,反对方则说这样不优雅,但是优雅这个词不足以反驳提问同学
因此这里题外说下为啥不能批量替换:
1、多人开发时候,批量替换导致大量冲突,且无法替换别人分支的代码,解决不好非常容易导致bug
2、即使只有自己开发,如果经常换实现类的话,总不能来来回回地批量替换吧
越抽象的东西变化越少
不依赖具体实现应该依赖什么呢?
应该依赖抽象,那抽象又是什么呢? 抽象是哲学中的逻辑学里面的概念,抽象是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征,举例来说,刀子可以伤人,枪也可以伤人,弓箭也可以伤人,从这些事物里面抽取共同的特性就是都能伤人,然后给这种东西创建一个概念叫做武器,武器就是一个抽象的概念,而刀是武器中的一个具体实现。
比如开发一个游戏,人物持有的装备,肯定是一个抽象,而不是一个具体的东西
class Hero{
private Weapon weapon;
}
同样道理,我们在写业务代码的过程中也不应该依赖具体实现
class BusinessService{
private Log log;
private Database database;
private HttpClient client;
}
我们常用的mysql、redis、httpclient,日志库、路由、实验开关等等其实都应该依赖抽象,而不是具体的实现。从哲学角度来说抽象本来就是从变化中总结出来的,因此抽象本身就是抵抗变化的。
依赖抽象(接口)就能更好地应对变化
因为这些组件都有非常多的具体实现,例如数据库不止mysql,万一哪天改成了postgre,缓存也不止有redis,httpclient、日志库等组件就更多了,如果一个项目都是依赖这些具体的实现,那么修改和迁移起来就变得非常困难
比如你在A公司写了个项目,但是A公司用mysql,A公司有自己的http库,自己的日志库,自己的日志规范。当想把这个项目在B公司跑起来的时候(先不考虑RCCD),发现跑不了呀,B公司用oracle,有另外的日志规范,有自己的服务发现框架等等完全不一样了,如果当时代码里都是写死了这些依赖,那么改动会非常大,但是如果依赖的是抽象,只需要更换个实现类就可以了,非常方便,这也就是“面向接口编程”的好处。
再比如,你依赖一个外部service,但是对方还没写好,这时候你需要写一个mock的实现去替换,不然他会一直卡着你无法完成后续的开发,这时候通过依赖注入,可以轻松地更换成你写的mock代码,到时候对方ready后,再换掉即可。
在业务代码中也存在非常多的依赖关系,原则上只要变化的地方,都应该依赖抽象(接口),除了容易更换外也更容易从业务角度去设计代码,也就是所谓的DDD(领域驱动设计),在设计业务代码的过程中,不再关心如何CRUD如何设计表结构,而是focus在业务本身,去设计抽象的业务逻辑例如下订单:
AntiCheat();
CouponCheck();
CreateOrder();
Notification();
抽象角度来看,一个订单可能涉及反作弊检查,优惠券检查,创建订单,通知用户,至于还涉及哪些环节就是需要领域专家决定了,领域专家对这个领域越是熟悉越能更好地抽象,所以DDD引入了领域专家,把专家的抽象经验拿过来用,从而降低变化的风险, 抽象层面并不关心这些东西怎么存储,怎么设计,怎么通知用户,这些具体的实现可能又能拆解出无数的抽象接口,最终由无数的实现类组装得到一个应用,类似手机、电脑组装,这就是面向接口编程另一个好处,关注分离
小结:
1、依赖注入是实现面向接口编程的重要手段
2、面向接口编程可以方便更换实现,使得关注点分离
网友评论