追本溯源,不断的回顾基础对我而言是种不错的方式,每次重新回顾这些点往往收获很大.以前,受个人所限,觉得这些理论毫指导价值价值,过于相信实践的的力量,导致自己进步缓慢.其实有些时候,实践更需要站在理论巨人的肩膀,这会让我们少走很多的弯路.
当然具体因人而异.
开发之困
实际开发中最常遇到的问题是类A直接依赖类B.当我们希望将类A修改为依赖类C时,就必须要通过修改类A来实现.这种 情况下类A作为高层的业务模块,负责复杂的业务模块,而类B和类C是底层模块,负责基本的原子操作.实际工程中类A作为业务模块,往往是非常复杂,如果修改类A可能会牵一发而动全身,
,进而带来不必要的业务风险.
那么这类问题该如何应该呢?下来看看大师们提出的原则:依赖倒置.
那什么是依赖倒置呢?
依赖倒置(Dependence Inversion Principle,简称DIP),其定义如下:
High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
这句话讲的是什么呢,其实就是以下三点:
- 高层模块不应该直接依赖底层模块,两者都应该依赖抽象.
- 抽象不应依赖细节.
- 细节应该依赖抽象.
看起来有点玄奥,实则不然,重点是理解其中谈到的高层,底层,抽象和细节分别是什么.
高层:通常是系统的负责具体业务逻辑的,它通常依赖一些更为底层的模块;从调用的角度来说,高层是调用者,底层是被调用者.在最常见的用户管理系统中,权限控制逻辑相对数据存储是高层模块,而数据模块则是底层模块,权限控制模块调用数据存储模块.
现在再来看"高层模块不应该依赖底层模块,两者都应该依赖抽象".放在这里就是,权限控制模块中不应该直接含有数据库链接的对象,同时高层和底层都应该具有其抽象更高层,这样的好处在与"越抽象,越高层,变化的可能性越小".如果你了解EIT模型,你会发现,EIT模型严格遵循了这一点.
如果说高层和底层是从宏观业务的角度来看,那么抽象和细节则更偏重实现的角度.当然,其实两者的存在紧密的联系,高层通常意味着抽象,底层则意味着细节.
抽象:在java中就是抽象类或者接口,两者无法直接实例化,必须通过其子类.这样的好处在于一个抽象(抽象类或者接口)可能存在多种子类,这中一对多的方式明显比1对1更具有选择性,而更多的选择性意味这灵活.
细节:即具体的实现类,也就是上面提到的子类,可以通过new关键字直接创建响应的实例.
其实不难发现,依赖导致的核心就是面向抽象编程(面向接口属于面向抽象中的一环).
很多人谈到面向接口编程这,觉得已经足够了,实际上我们可以更简练一点:面向抽象编程的就是为了解决对象之间的直接依赖.
相信你现在应该对DIP有认识了,但是对于刚接触的开发者而言,依赖这个词看起并不是那么友好.所以,仍然有必要再对其通俗话.
像小学生造句一样,我们利用依赖这个词造个句子:小颜依赖电脑工作.
这个句子中出现了"小颜","电脑"两个对象,这句话的意思也就是,小颜需要主动借助电脑才能工作,换句话说,电脑影响小颜的工作.
在实际生活中,我们几乎每时每刻的都在依赖其他事物(对象)帮我们达成目的,一旦我们所依赖的事物发生改变(事物消失,内部结构变化等等),我们就不得不改变原有做事的方式.
在软件工程中同样如此,每个功能的实现,都意味着不同的对象相互依赖.其中,在某些"坏代码",你会发现几个变化频繁的对象竟然被直接耦合在一些,这种情况下,后果可想而知.
困难才露尖尖角
到现在,你已经重新温习了dip原则,也许还在努力尝试背下来?现在呢,我希望你忘记上面所说的一切,忘记所谓的准确准则.
现在我们来考虑这样么一种简单的需求:男人开奥迪.相信你很快用OOD的思想,直接划分出Man类和Audi类.那么紧接着,你会写出如下的代码:
public class Audi {
public String getName() {
return "audi";
}
}
public class Man {
public void drive(Audi audi) {
System.out.println("man drive audi");
}
}
public class Client {
public static void main(String[] args) {
Audi audi = new Audi();
Man man = new Man();
man.drive(audi);
}
}
ok,我们很快写完了这段代码,现在回过头来想想发生了什么,你又没有觉得在正式开始编写Man类之前需要编写Audi类?
现在呢,正当你感慨于自己写代码之快之际,你的客户说,女人也可以开车啊,不但可以开Audi还可以开QQ呢.你一想,这还不容易,改改代码不久很快就解决了?改改代码当然可以解决,但是你刚改完,你的老板却说女人开车多不好,还是不让她开了吧.于是,你一边大骂领导**,一遍含泪改代码.如果到现在,你还可以忍的话,那下面的需求你可能就有点想发狂的冲动了"男人女人都可以开各种各样的车"?
到现在,相信你也不难发现,让你发怒的不是频繁的需求变动,而恰恰是不断的改代码,大体就是:修改-推到-复制等一系列无意义的修改操作,换句话说上面的代码太过于脆弱了,那么脆弱的原因是什么呢,恰恰就是Man和Audi直接耦合性太高了,Man中需要Audi,所以你就在Man中粗暴的new了一个Audi的对象,不是吗?
上面仅仅是一个简单的demo,在实际工程中往往是数万行的模块,这时候,修改的代码的工程量和风险可就不像现在这么简单了.
早有方案立上头
好吧,为了解决这个问题该怎么办呢?我们来尝试寻求一种"以不变应万变"的方案.那么在上面的代码中"变"发生的地方在哪里呢?不难发现恰好是Man和Audi,那解决方案就很明显了,将Man和Audi做成不变的,很快,你就想到了接口.让我们试试能不能解决:
public interface Human {
void drive(Car car);
}
public interface Car {
String getName();
}
public class Audi implements Car{
@Override
public String getName() {
return "audi";
}
}
public class Man implements Human {
@Override
public void drive(Car car) {
System.out.println("man drive " + car.getName());
}
}
public class Client {
public static void main(String[] args) {
Human human = new Man();
Car car = new Audi();
human.drive(car);
}
}
一番折磨之后,我们将Man抽象为不变的Human,Audi抽象为不变的Car,通过Human和Car这两个抽象产生依赖来避免Man和Audi直接产生依赖.
现在再来想想我们上面提到的依赖倒置原则:
Human是高层模块Man的抽象,Car是底层模块Audi的抽象.不难发现高层的Man并不直接依赖底层的Audi,其两者都有各自的抽象,这就是上文提到的"高层不应该直接依赖底层,双方应该依赖抽象"
另外,Human和Car的子类的增删改,并不会影响Human和Car,这也就是所谓的抽象不依赖细节.
最后,Human和Car的子类必须实现其接口所定义的方法,这就是所谓的细节的实现依赖抽象.
到现在为止,相信你对依赖有了一些理解.实际上DIP这个词在软件开发早期是没有的,后来大家发现经常面对这种依赖问题,故而将其定义成规则.那么为什么又加上"倒置"一词的.
实际上,不难发现第一种代码是按我们常规思维写出来的(常规思维是线性的,人认识事物的本质是从具体到抽象),而二种则不然,因此"倒置"一词其实反映的是逆常规思维(从抽象到具体),即:将类与类直接打交道改编为抽象与抽象打交道,也就是所谓的面向抽象(抽象类或接口)编程.
控制反转和依赖注入
何为控制反转
熟悉Spring的开发者很快对控制反转(IOC)和依赖注入(DI)应该非常了解,在这里我并不准备多说.尽管如此,仍然简单的说明下:
控制反转即IOC,它把传统上由程序来直接操控的对象的调用权交给外部的容器去控制,通过容器实现对象的组件和装配.说白了,也就是将对象的控制权从程序本身转移到外部容器.这样说起来还是很抽象,那我们以最简单用户注册模块来说明.
大体的业务逻辑是,UserService负责用户信息的校验,对于校验通过的用户,通过UserDao存入数据库.那么代码大体如下:
public interface UserDao{
void insert(User user);
}
public class UserDaoImpl implements UserDao{
@override
public void insert(User user){
//存储的具体实现
}
}
public interface Uservice{
void storeUser(User user);
}
public class UserServiceImpl implments UserService{
private UserDao userDao;
public UserService(){
userDao=new UserDaoImpl();
}
@override
public void storeUser(User user){
//用户信息校验通过
userDao.insert(user);
}
}
不难在上面的代码中我们已经尽可能的遵循DIP原则,但是这样就够了么?
你会发现在UserServiceImpl中,为了我们通过new来创建userDao对象,即userDao这个对象的控制权实际在UserServiceImpl中.另外,我们知道凡是在一个类中用new创建另一种对象的地方也就意味着耦合,那么有没有其他的方式来解除这个耦合的?在不引入外部容器的情况下,好像暂无其他解决方案.既然这样,我们就引入一个第三方的容器,该容器存储了所有注册的对象.在运行时,代码哪里需要某个对象,就从容器中取出就好了.
回归上面的代码就是UserServiceImpl不再负责userDao对象的创建工作,userDao对象的创建交给了第三方容器,即userDao对象的控制权从UserServiceImpl转移到了第三方容器,这就是控制反转的内涵.
不难发现控制反转实在DIP原则上的进一步升级,旨在解决对象依赖这问题.
控制反转和依赖倒置皆提现了常规线性思维的转变.
控制意味着谁来控制对象的创建,即对象的控制者是谁?是程序本身,还是第三方容器?
反转则意味对象的控制者从程序本身变为第三方容器.
依赖注入
如果说控制反转是一种思维方式的变化,那么依赖注入则是该思想的具体实践.
到现在为止,实现控制反转通常有两种方式:依赖查找和依赖注入.
- 依赖查找:容器提供回调接口和上下文给组件,需要组件自己实现查找合适的对象的过程,此类以EJB为代表.
- 依赖注入:组件不做任何定位查询,只需向容器提供普通的java方法,然后容器根据这些方法自行决定依赖关系.此类以Spring为代表.
通过以上定义,不难发现两者虽然都实现了控制反转,但是依赖注入重在自动注入,更为自动和简单,基本能够实现对原有代码的无侵入,这也是为什么依赖注入更为流行的原因.