接上一篇“演进式例解控制反转(IoC)、依赖注入(DI)之一”的例子继续往下。
回顾:
上一篇文章演进式的问题描述、解决方法只有 3 个阶段,其中后面 2 个分别是引入了 Container、Service Locator 这样一种间接层,以便解决各个‘问题描述’中可能的不足之处(仅仅是‘可能’,或许系统不需要考虑这么麻烦的需求,是否因为引入间接层而增大系统不必要的复杂度得由具体需求所决定),也就是希望消除(或者说转移、减弱)一些直接依赖、紧耦合。
实际上一篇还未能引入 IoC 、DI,以其做铺垫热身之后的这篇才是重点要理解的。
问题描述:
然而,不管是引入 Container 还是使用 Service Locator ,ReportService 对于具体组件的查找、创建的方式都是‘主动’的,这意味着作为客户的 ReportService 必须清楚自己需要的是什么、到哪里获取、如何获取。一下子就因为 What、Where、How 而不得不增加了具体逻辑细节。
例如,在前面‘引入Container ’的实现方法中,有如下代码:
class ReportService {
// 消除紧耦合关系,由容器取而代之
// private static ReportGenerator generator = new PDFGenerator();
// 通过 Container..getBean("reportGenerator") ‘主动’查找
private ReportGenerator generator = (ReportGenerator) Container
.getBean("reportGenerator");
在‘引入 Service Locator ’的实现方法中,有如下代码:
class ServiceLocator {
privatestatic Container container = new Container();
publicstatic ReportGenerator getReportGenerator() {
// 还是container.getBean(), 用了委托而已
return (ReportGenerator) container.getBean("reportGeneraator");
}
}
// ReportService 最终还是‘主动’查找,委托给ServiceLocator 而已
private ReportGenerator reportGenerator = ServiceLocator.getReportGenerator();
}
在这种情况下,变‘主动’为‘被动’无疑能够减少 ReportService 的内部知识(即查找组件的逻辑)。根据控制反转(IoC)原则,可江此种拉(Pull,主动的)转化成推(Push,被动的)的模式。
例如,平时使用的 RSS 订阅就是Push的应用,省去了我们一天好几次登录自己喜爱的站点主动获取文章更新的麻烦。
而依赖注入(DI)则是实现这种被动接收、减少客户(在这里即ReportService)自身包含复杂逻辑、知晓过多的弊病。
因为我们希望是‘被动’的接收,故还是回到 Container 的例子,而不使用 Service Locator 模式。由此得到修改后的类图如下:
而原来的类图如下,可以对照着看一下,注意注释的提示:
为了使例子能够编译、运行,并且稍微利用跟踪代码的运行结果来显式整个类图实例化、互相协作的先后顺序,在各个类的构造器中加入了不少已编号的打印语句,以及两个无关紧要的类,有点啰唆,具体如下:
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
// 为了能够编译运行,多了两个无关紧要的类
class Month { }
class Table {
void setDate(Date date) { }
void setMonth(Month month) { }
}
// ------------ 以下均无甚重要改变 ----------------- //
interface ReportGenerator {
void generate(Table table);
}
class ExcelGenerator implements ReportGenerator {
public ExcelGenerator() {
System.out.println("2...开始初始化 ExcelGenerator ...");
}
void generate(Table table) {
System."generate an Excel report ...");
}
}
class PDFGenerator public PDFGenerator() {
System. PDFGenerator ...");
}
"generate an PDF report ...");
}
}
//------------ 以上均无甚重要改变 ----------------- //
class Container {
// 以键-值对形式保存各种所需组件 Bean
static Map<String,Object> beans;
public Container() {
System."1...开始初始化 Container ...");
beans = new HashMap<String,Object>();
// 创建、保存具体的报表生起器
ReportGenerator reportGenerator = new PDFGenerator();
beans.put("reportGenerator",reportGenerator);
// 获取、管理 ReportService 的引用
ReportService reportService = new ReportService();
// 注入上面已创建的具体 ReportGenerator 实例
reportService.setReportGenerator(reportGenerator);
"reportService",reportService);
System."5...结束初始化 Container ...");
}
static Object getBean(String id) {
System."最后获取服务组件...getBean() --> " + id + " ...");
returnbeans.get(id);
}
}
class ReportService {
// 消除上面的紧耦合关系,由容器取而代之
// private ReportGenerator generator = (ReportGenerator) Container
// .getBean("reportGenerator");
// 去除上面的“主动”查找,提供私有字段来保存外部注入的对象
private ReportGenerator generator;
// 以 setter 方式从外部注入
void setReportGenerator(ReportGenerator generator) {
System."4...开始注入 ReportGenerator ...");
this.generator = generator;
}
private Table table = new Table();
public ReportService() {
System."3...开始初始化 ReportService ...");
}
void getDailyReport(Date date) {
table.setDate(date);
generator.generate(table);
}
void getMonthlyReport(Month month) {
table.setMonth(month);
table);
}
}
class Client {
staticvoid main(String[] args) {
// 初始化容器
new Container();
ReportService reportService = (ReportService) Container
.getBean("reportService");
reportService.getDailyReport(new Date());
// reportService.getMonthlyReport(new Date());
}
}
运行结果:
1...开始初始化 Container ...
2...开始初始化 PDFGenerator ...
3...开始初始化 ReportService ...
4...开始注入 ReportGenerator ...
5...结束初始化 Container ...
最后获取服务组件
...getBean() --> reportService ...
generate an PDF report ...
注意:
1、根据上面运行结果的打印顺序,可见代码中加入的具体编号是合理的,模拟了程序执行的流程,于是也就不再画序列图了。
2、注意该例子中对IoC、DI的使用,是以ReportService为客户端(即组件需求者)为基点的,而代码中的Client 类main()中的测试代码才是服务组件的最终用户,但它需要的不是组件,而是组件所具有的服务。
3、实际在Spring框剪中,初始化Container显然不是最终用户Client应该做的事情,它应该由服务提供方事先启动就绪。
4、在最终用户Client中,我们还是用到Container.getBean("reportService")来获取事先已在Container的构造函数中实例化好的服务组件。而在具体应用中,通常是用XML等配置文件将可用的服务组件部署到服务器中,再由Container读取该配置文件结合反射技术得以创建、注入具体的服务组件。
分析:
之前是由ReportService主动从Container中请求获取服务组件,而现在是被动地等待Container注入(Inject,也就是Push)服务组件。控制权明显地由底层模块(ReportService 是组件需求者)转移给高层模块(Container 是组件提供者),也就是控制反转了。
回头看看上一篇文章吧:-D,应该更能帮助理清例子的演进历程。
演进式例解控制反转(IoC)、依赖注入(DI)之一
其他2种依赖注入方式:
上面用到的是setter方式的依赖注入,还有constructor方式的构造器注入、接口注入。
1、constructor 方式
与setter 方式很类似,只不过有所差异,例如:如果有过多组件需要注入,constructor方式则会造成参数列表过长;也比较僵化,因为该注入只发生在构造期,而setter 方式或者比较灵活些,需要时则注入。
2、接口方式
据说该方式的注入不常用,一些IoC框架如Spring也不怎么支持,问题在于其真的是比较麻烦:定义特定interface,并声明所需接口(即待实现的Method),最后组件类通过实现该interface 中的特定Method 进行组件依赖注入。既然少用,也不给出代码了。
小结:
感觉按照着逐步演进的步骤来理解一个问题的出现、分析原因、解决、分析结果是比较容易接收的,你觉得呢?