谈起 Spring 容器,一般都会提到两个特征,一个是 IoC(Inversion of Control)或者 DI(Dependency Injection),以及 AOP(Aspect Oriented Program)。控制反转和面向切面编程两个概念虽然是 Spring 涉及的两个非常基础的概念,但是要真正透彻理解这两个概念还是有一定的成本。
Spring 的面向切面编程,不仅概念比较抽象,而且从原理上也比较难讲清楚其具体的实现方式。主要是因为,其实现上涉及了诸如动态代理、字节码生成、反射等比较“冷门”的底层技术,同时,Spring 的具体实现还涉及了很多设计模式。不是我们讨论的重点。
本文主要讨论对控制反转的理解。
要讨论一个概念,一般先从字面上开始理解。相比于面向切面编程中的“切面”,“控制反转”涉及的两个词语都比较常见,可以说比较好认知。汉语比较博大精深,如果仅从汉语的字面上来看,“控制反转”既可以理解为“控制某些事情的反转趋势”,也可以理解为“反转某些控制权”,因为“控制”和“反转”这两个词,既可以作为名词,又可以作为动词。这里可能稍微要吐槽一下这个翻译,假如将其翻译为“反转控制”可能更好,因为这样的语序不容易有两种理解。
基于已有的基本了解,很明显,“控制反转”说的是“反转某些控制权”。那么,这里反转的控制权到底说的是什么?为什么要进行反转?是谁反转了谁?反转从整体上看带来了哪些好处呢?
为了方便讨论,这里假设一种场景:某公司 S 提供商品售卖服务,但是不具备支付资质,因此需要与其他支付公司合作。项目初期,与支付公司 A 达成合作。为了完成商品售卖在 S 公司的订单服务中需要调用 A 公司的支付接口,这个时候代码看起来很可能是这样(代码仅做示意,不考虑现实约束与可行性):
/**
* 订单服务
*/
public class OrderService extends ACompanyPayService {
private ACompanyPayService payService;
/**
* 订单创建
* @param order
* @return
*/
public boolean createOrder(Order order) {
int amount = order.getNum() * order.getPrice();
payService = new ACompanyPayService();
payService.pay(order.getUserId(), amount);
// creat order and other thing else...
return true;
}
}
复制代码
/**
* A公司支付服务
*/
public class ACompanyPayService {
/**
* pay by account.
* @param amount
* @return
*/
boolean pay(int userId, int amount) {
Account account = getUserAccount(userId);
if (amount > account.getAmount()) {
return false;
} else {
account.setAmount(account.getAmount() - amount);
}
return true;
}
private Account getUserAccount(int userId) {
// get user account by userId;
return null;
}
}
复制代码
这个时候类之间的依赖关系应该是下图这个这样子,订单服务依赖于 A 公司的支付服务,控制方向是从上到下的。
现在,由于业务的发展,S 公司又发展了合作伙伴 B 支付公司,如果这个时候不做设计优化,那么很可能订单服务的代码会变成这样
public class OrderService {
private PayService payService;
public boolean createOrder(Order order) {
int amount = order.getNum() * order.getPrice();
choosePayService(order.getUserId());
payService.pay(order.getUserId(), amount);
// creat order and other thing else...
return true;
}
/**
* 根据用户id选择支付服务
*/
private void choosePayService(int userId) {
int accountType = getUserAccountType(userId);
if (1 == accountType) {
payService = new ACompanyPayService();
} else {
payService = new BCompanyPayService();
}
}
/**
* 获取用户账户类型
*/
private int getUserAccountType(int userId) {
// decide user account type by user id.
return 1;
}
}
复制代码
那么这个时候,类的关系会变成这样:
这个时候订单服务同时依赖于 A\B 两个支付服务,假设后期由于业务发展接入更多的支付公司,那么很容易想到 OrderService 中的代码必然在某处增加更多的分支,并且每次一旦接入新的支付公司,都需要修改 OrderService 这个核心代码,成本和风险都不低。这里很明显违反了 OCP 原则,对于每次新增支付公司接入,都需要“修改”代码,而不是“扩展”代码。
随着代码维护难度越来越高,订单服务的工程师终于无法忍受这种代码结构,于是进行了重构。重构后的核心代码可能变成了这样:
/**
* 订单服务
*/
public class OrderService {
private PayService payService;
private PayServiceFactory payServiceFactory;
public boolean createOrder(Order order) {
int amount = order.getNum() * order.getPrice();
choosePayService(order.getUserId());
payService.pay(order.getUserId(), amount);
// creat order and other thing else...
return true;
}
private void choosePayService(int userId) {
int accountType = getUserAccountType(userId);
payService = payServiceFactory.getPayServiceByAccountType(accountType);
}
private int getUserAccountType(int userId) {
// decide user account type by user id.
return 1;
}
}
复制代码
/**
* 支付接口定义
*/
public interface PayService {
/**
* 用户账户支付
* @param userId 用户ID
* @param amount 支付金额
* @return
*/
public boolean pay(int userId, double amount);
}
复制代码
/**
* 支付服务工厂类
*/
public interface PayServiceFactory {
/**
* 根据账户类型获取支付服务
* @param accountType
* @return
*/
public PayService getPayServiceByAccountType(int accountType);
}
复制代码
/**
* 支付工厂类实现
*/
public class PayServiceFactoryImpl implements PayServiceFactory {
public PayService getPayServiceByAccountType(int accountType) {
PayService payService;
if (1 == accountType) {
payService = new ACompanyPayService();
} else {
payService = new BCompanyPayService();
}
return payService;
}
}
复制代码
这个时候的类关系图会是下图这样,通过定义两个接口PayService
和PayServiceFactory
,OrderService
得以解除对具体支付服务的依赖,从图上看,依赖关系从原先的从下到上,变成了跨越红线的从上到下的依赖关系,从而实现了反转。
在现实中,接口PayService
和PayServiceFactory
的定义将由 S 公司给出,接口的具体实现交由支付公司实现,这样 S 公司实现了控制权的反转,不再依赖于具体的支付公司支付服务实现的细节。
在 Spring 容器里面,具体的工厂类及其实现(对照上图中的PayServiceFactoryImpl
和PayServiceFactory
)由框架完成,用户只需要通过 XML 定义或者注解的方式声明接口和类,不再需要管理类之间依赖关系和具体实例的创建。
控制反转实际上不仅仅是程序设计中的一个原则,在日常的管理当中也普遍存在。
例如,在质量审计当中,要求所有的软件项目都必须在项目实施过程中按照一定的标准产出过程文档,以便对其实施过程的质量进行监测。这里的过程文档标准就可以类比为程序实现中的“接口”,质量审计部门通过定义这个“接口”来反转对每个具体项目的依赖,由每个项目依赖于统一的标准。
再例如,在日常的工作汇报中,上级管理者往往要求下级按照一定的“模板”对工作进行汇报,这里的“模板”便是反转依赖的关键,通过规定这个“模板”,管理者省去了从各个汇报者给出的不同格式的汇报信息中挑选有用信息的加工过程,从而反转了控制权。
回答一开始提出的问题。总的来说,控制反转是为了将原先不够灵活可扩展的依赖关系进行反转,从整体上看原先的被控制者(依赖者)变成了控制者(被依赖者),从而可以降低甚至消除原先的依赖者管理冗杂信息的成本,通过定义标准/接口,变成了管理标准而不是管理实现,从而解放了原先依赖者。
评论