Spring 5企业级开发实战
上QQ阅读APP看书,第一时间看更新

2.1 IoC容器揭秘

2.1.1 IoC的概念

IoC是Inversion of Control的简写,翻译成汉语就是“控制反转”。IoC并不是一门技术,而是一种设计思想。在没有IoC设计的场景下,开发人员在使用所需的对象时,需手动创建各种对象,如new Student()。如图2-1所示是传统Java开发方式。

有了IoC这样的设计思想,在开发中,意味着将设计好的对象交给容器管理,而不再是像传统的编程方式中,在对象内部直接控制对象。如图2-2所示是使用IoC的Java开发方式。

如何理解IoC呢?从IoC的字面意思上,无非是要把握好两块:控制、反转。下面将从这两方面着手分析IoC。

1. 控制:由谁控制,控制了什么?

在传统Java程序设计中,开发人员直接在某个对象内部通过new关键字创建另一个对象,是开发人员主动去创建依赖对象;而IoC的设计思想,是通过专门的对象容器来创建和维护对象。于是就可以解答这个问题了。对于谁控制这个问题的回答是,由IoC容器来控制;对于控制了什么这个问题的回答是:控制了对象。

图2-1 传统Java开发方式

图2-2 使用IoC的Java开发方式

2. 反转:什么是反转,反转了哪些方面?

在传统Java应用程序开发中,由开发人员在对象中主动控制其需要的依赖对象;而反转则是把对象依赖的过程颠倒了,即开发人员不再控制其依赖对象,而是由容器来帮助开发人员创建其需要的对象。于是就可以解答这个问题了。对于什么是反转这个问题的回答是,由容器帮开发人员创建依赖对象,对象只是被动地接受依赖对象,对象的控制权不再是开发人员,而是容器;对于反转了哪些方面这个问题的回答是,开发人员需要依赖的对象被反转。

现在再来回顾一下IoC的概念就好理解了,IoC——控制反转,即对象的控制权转移了,从开发人员转移到对象容器了。

2.1.2 依赖倒置原则

软件工程理论中的六大设计原则。

1. 单一职责原则

不存在多于一个的因素导致类的状态发生变更,即一个类只负责一项单一的职责。

2. 里氏替换原则

基类出现的地方都可以用其子类进行替换,而不会引起任何不适应的问题。

3. 接口隔离原则

客户端不应该依赖于其不需要的接口,类间的依赖关系应该建立在最小的接口之上。

4. 迪米特法则

一个对象对其他对象有最少的了解。

5. 开闭原则

软件设计对于扩展是开放的(Open for extension),即模块的行为是可以扩展的。

软件设计对于修改是关闭的(Closed for modification),即模块的行为是不可修改的。

6. 依赖倒置原则

高层次的模块不应该依赖于低层次的模块,都应该依赖于抽象。

如图2-3所示,CTO是整个组织架构的高层模块,其他模块都是CTO的底层模块。同理,研发1部是业务研发1组和业务研发2组的高层模块,业务研发1组和业务研发2组是研发1部的底层模块,以此类推。

图2-3 组织架构示意图

抽象不应该依赖于具体实现,具体实现应该依赖于抽象。例如,人就是一个抽象,具体到黄种人、白种人和黑种人就是具体的实现。具体对应到软件工程领域,抽象可以是抽象类或接口。具体实现就是继承或实现这些抽象类或接口的类。如下代码就是一个典型的抽象和具体实现:

通过上述代码,分析了依赖倒置原则的定义后,下面将对比不遵循依赖倒置原则和遵循依赖倒置原则的两种不同软件设计风格。

假设有如下场景,一个人,需要通过某种交通工具去上班。在不遵循倒置原则的情况下,软件的设计风格如下。

当员工住的离公司比较近的时候,骑自行车上班即可:

当员工住的离公司远,需要改乘公交上班时,代码需要修改成如下:

如果员工买车,就不需要坐公交上班,这时对于Person要加入新的对象如Car来满足新的需求。

如图2-4所示,按照以上的案例设计,对于每一次的需求更新,对Person类的修改太多,几乎每一次需求变更,都要对Person进行一次重构,显然这种做法不合适。通过分析可以得知,Person是依赖于Bike、Bus的。Person类的go()方法功能完全依赖于bike和bus属性的go()方法。直接依赖于具体实现,带来的后果就是每次需求变更,必然要对代码进行重构。

图2-4 传统Java开发方式

接下来对上述的设计方法进行优化,对Person引进抽象。将Person需要使用的交通工具抽象成一个接口Movable,接口中有个go()方法。具体代码如下:

让Person依赖Movable接口的实例对象,当需求变更后,对Person的改动很小。Person依赖Movable,对Person的改动如下:

Person中的go()方法依赖Movable抽象,在Person内部不确定指明其依赖的具体某种交通工具(如Bike、Bus、Car等),至此可以说,Person这个上层类,已经不直接依赖于底层类了,实现了依赖抽象的设计风格。

Movable是抽象,其代表了一类行为。Bike、Bus、Car是具体的实现细节,它们有自己的个性化部分——Bike需要用脚踩,Bus需要投币,Car需要加油等,但是它们都有一个共性——都可以用作上班时的交通工具。抽象不应该依赖于具体实现,具体实现应该依赖于抽象,即定义交通工具Movable时,不应该依赖于每种具体交通工具的具体功能,反之,每种交通工具(Bike、Car、Bus等具体的实现),应该依赖抽象Movable。如图2-5所示,当用户想要乘坐地铁上班时,按照依赖倒置的软件设计风格,Person类可以轻松地实现扩展。其实依赖倒置是软件工程中面向接口编程的一种具体实现。

图2-5 依赖倒置的软件设置风格

2.1.3 依赖注入

通过以上小节的分析,软件设计虽然采用依赖倒置的原则,但是,对象所依赖的其他对象,还是需要研发人员手动管理,即Person依赖的Bike、Bus、Car,还是需要开发人员主观创建这些依赖对象。有没有办法实现无须开发人员手动管理依赖关系呢?这就是依赖注入。

依赖注入(DI,Dependency Injection)是Spring实现IoC容器的一种重要手段。依赖注入将对象间的依赖的控制权从开发人员转移到了容器,降低了开发成本。此时,Person不再需要开发人员手动维护其依赖项,而是通过Spring将依赖关系注入Person中。一个简单的依赖注入代码如下:

“@Service("bike")”表示将Bike交给IoC容器管理,容器中会存放一个符合单例模式的Bike对象。当Person依赖Bike时,无须开发人员手工维护Person和Bike的依赖关系,IoC容器很好地解决了这个问题,代码如下:

图2-6可以更好地帮助理解依赖注入。开发人员手工管理依赖,类似于顾客主动向服务员要取菜单,“服务员,把菜单给我,我要点餐”。通过依赖注入这种设计方案,顾客无需向服务员索要菜单,服务员将会走到顾客身边,将菜单递给顾客。回到软件开发场景中,通过依赖注入,Person类不再需要创建其依赖的交通工具,当Person需要借助交通工具去上班时,直接使用Movable对象就可以实现,具体是通过何种交通工具上班,开发人员无须再关心。

图2-6 依赖倒置示意图

依赖注入减少了开发人员维护大量依赖关系的工作量,提高了开发人员的工作效率。

控制反转、依赖倒置和依赖注入三者之间的关系是:控制反转是一种软件设计模式,其遵循了软件工程中的依赖倒置原则;依赖注入是Spring框架实现控制反转的一种方式。