二、依赖注入的应用模式
前面我们了解了依赖注入的基本概念,也对一些依赖注入框架进行了简单的介绍,这一章我们主要来讨论作为开发者如何利用依赖注入框架来实现依赖注入的设计思想。
1. 依赖注入的方式
前面我们提到,所谓“依赖”,最简单地去解释就是一个Java类里的成员变量。我们都知道,给一个类中的私有成员变量赋值的方法通常有:通过Constructor构造方法、通过Setter方法、通过反射机制将私有变量的可见性设为true这三种方法。同样道理,依赖注入框架也是利用这三种方式来完成依赖对象的设定的,我们分别称之为Constructor注入、Setter注入、成员变量注入。
这里有两点需要注意的地方。一是除了这三种常见的注入方式,还有一些其他的注入方式,例如Spring框架中提供的lookup-method为代表的方法注入模式,也被称作“装饰器”模式,还有像Guice中的方法参数注入等等,我们在此不再详加讨论。二是并不是所有的依赖注入框架都提供这三种常见的注入方式,比如Seam2.x版本里就不支持Constructor注入的方式。
1.1. Constructor注入
利用Constructor注入,即在依赖者类的构造函数上声明注入点,而要被注入的对象则是构造函数的参数。
我们来看利用Spring框架的例子。首先我们将Bank和DepositBook两个依赖类声明为Spring容器管理的组件:
@Component // 声明此依赖为Spring组件
public class BankICBC implements Bank { // …… }
@Component // 声明此依赖为Spring组件
public class DepositBookICBC implements DepositBook { // …… }
|
|
这里我们采用Spring
3.x提供的@Component方式来声明Spring组件,当然还有很多其它的方式,就不再详细阐述了。将这两个依赖声明为Spring容器管理的组件之后,它们就可以在被需要的时候由容器提供给依赖者了。例如在储户Depositor类中,我们定义了以Constructor为注入点的依赖请求:
@Component
public class Depositor {
@Autowired // Constructor注入点
public Depositor(Bank bank, DepositBook
depositBook) {
this.bank = bank;
this.depositBook = depositBook;
}
private Bank bank;
private DepositBook depositBook;
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook,
amount);
}
}
|
|
之后主程序在调用这个Depositor类的时候,容器就会自动将保管的依赖对象设定好之后分发给主程序了。
public void runSpringConstructorDI() {
AnnotationConfigApplicationContext context =
new
AnnotationConfigApplicationContext("tutorial.di.ch01");
Depositor depositor =
context.getBean(Depositor.class);
depositor.withDraw(new
BigDecimal(10000));
}
|
|
1.2. Setter注入
第二种常用的声明注入点的方法是Setter注入,即依赖注入框架利用反射机制调用依赖者类的Setter方法来完成依赖注入。
我们来看利用Seam框架的例子。首先还是要将Bank和DepositBook两个依赖类声明为Seam容器管理的组件:
@Name("bank")
public class BankICBC implements
Bank { // …… }
@Name("depositBook")
public class DepositBookICBC implements DepositBook { // ……
}
|
|
之后在依赖者Depositor类中的Setter方法处声明注入点:
@Name("depositor")
public class Depositor {
private Bank bank;
private DepositBook depositBook;
@In // bank的Setter注入点
public void setBank(Bank bank) {
this.bank = bank;
}
@In // depositBook的Setter注入点
public void setDepositBook(DepositBook
depositBook) {
this.depositBook = depositBook;
}
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook,
amount);
}
}
|
|
注意Seam框架缺省是将与该依赖字段同名的标识符所绑定的依赖对象注入进来,如果想注入与字段名不同的标识符得依赖对象,需要使用@In(“依赖对象标识符”)这样的语法。
1.3. 成员变量注入
成员变量注入是指依赖注入框架利用反射机制,直接将依赖者类的成员变量set为容器管理的依赖对象。
仍以Seam框架为例,与刚才Setter注入模式唯一不同的就是将@In标注在成员变量旁,而不是其Setter方法上。
@Name("depositor")
public class Depositor {
@In // bank的成员变量注入点
private Bank bank;
@In // depositBook的成员变量注入点
private DepositBook depositBook;
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook,
amount);
}
}
|
|
1.4. 注入模式的选择
对于该怎么使用前面介绍的这几种注入模式,究竟在什么情况下该使用哪种,这是一个讨论非常广泛的话题。这里只举一个简单的例子来说明,比如要设计一个不可变(final)依赖的类,则必须使用Constructor注入方式:
@Component
public class Depositor {
@Autowired // Constructor注入点
public Depositor(Bank bank, DepositBook
depositBook) {
this.bank = bank;
this.depositBook = depositBook;
}
private final Bank bank; // 不可变的依赖
private final DepositBook depositBook; //
不可变的依赖
public Cash withDraw(BigDecimal amount) {
return bank.withDraw(depositBook,
amount);
}
}
|
|
再如如果一个类所拥有的依赖数量过多、而开发者又不想构造方法的参数成为“过长参数列表”(这是一种影响代码的可读性和可维护性的反模式,Martin
Fowler于1999年在其《Refactoring:Improving the Design of Existing
Code》一书中提出),则可考虑Setter注入或者成员变量注入。此外,如果遇到了类似“循环依赖”的情况,例如宿主和寄生动物这两个对象就属于相互依存的“循环依赖”模式,这样的情况下,我们就可能需要Constructor和Setter两种混合的模式才能解决。总而言之,选择依赖注入模式是一个很大的话题,我们在此就不再深入讨论了。