考虑在你的程序中集成
picocontainer或spring...框架
实现和接口分离,使用和组装分离是一个基本的对象设计原则,简单的工厂方法(gof)、服务定位器模式(j2ee核心模式)已经被广泛使用,近来由于测试驱动方法的深入人心,有洁癖的程序员们又重新理解了ioc(Inversion of Control),并把它们变成实现,代表性的实现有PicoContainer,Spring,而Martin
Fowler也趁机总结出了一个新的模式:Dependency
Injection
,让我们别停留在理论与争论了,看看怎样用它来实际的简化我们的程序才是正解,用过之后再吵个翻天也不迟。本文将通过一个数据库访问层和web层的集成来应用picoContainer.让我们这就来看它的威力吧。
本文假设你具有junit单元测试/web框架,使用经验,最好了解webwork
1机理,不过他简单的你甚至可以现在才了解。
先看看文中所用的东东:
- webwork 1 :
一个非常简单的web框架,核心接口是
Action.execute(),我们将实现之来处理每一次web层的action(也就是一次post)
。
-
dao模式(j2ee核心模式) :他将封装数据库访问的所有细节。
让我们来看看通常我们实现一个简单的用户登陆过程所要做的所有事情:

从上图可知LoginAction是我们程序的顶层类,它依赖UserDao来完成登陆业务逻辑,ok,这就是一个典型的web应用,一次请求发送到web
server,然后由webwork框架接管,他按照一个配置文件把相应的登陆请求对应到LoginAction类上,然后用其缺省的构造器实例化LoginAction,然后把post上来的表单值或url参数值按名称填充到LoginAction(即:name,password),然后调用命令模式的接口Action.execute()完成调用,让我们来看一下实际代码:
public interface UserDao {
public User load(String username);
}
|
import webwork.action.ActionSupport;
import ftsmen.dao.UserDao;
import ftsmen.entity.User;
public class LoginAction extends ActionSupport {
private String username;
private String password;
private UserDao userDao ;
public LoginAction() {
//依赖对象在这里初始化
userDao = DaoFactory.createUserDao();
}
//为了程序的简单,假设用户总是已经注册过的
public String execute() {
User u = userDao.load(getUsername());
if (!u.verifyPassword(getPassword())) {
//密码错误;
return ERROR;
}
//验证通过......可以把用户信息放在session中
return SUCCESS;
}
public String getUsername() {
return username;
}
public void setUsername(String string) {
username = string;
}
public String getPassword() {
return password;
}
public void setPassword(String string) {
password = string;
}
}
|
对象知识告诉我们要让接口和实现分离,于是我们就用工厂方法隐藏了UserDao的实现和初始化细节。我们将如何测试LoginAction而不依赖UserDao的数据库实现呢?我们知道单元测试的一个常用方法是:用mock object替换待测试类的依赖对象,具体使用可以参考MockObjects、JMock.
我们这里用一个子类来充当mock,但究竟怎样把这个mock object替换LoginAction中的那个userDao呢???

目前常用的3种方法都可以做到,参考Dependency
Injection:
1.服务定位器(Service Locator):
我们可以把DaoFactory简单的改造为服务定位器:
public class DaoFactory{
private static ThreadLocal instance = new ThreadLocal();
private UserDao userDao;
public DaoFactory(UserDao userDao) {
this.userDao =userDao ;
}
private DaoFactory() {}
public static void load(DaoFactory locator) {
instance.set(locator);
}
public static UserDao createUserDao() {
return ((DaoFactory)instance.get()).userDao;
}
}
|
这样就可以不改变原来的LoginAction代码,并可以把mock UserDao插入到待测类LoginAction中:
public class LoginActionServiceLocatorTest extends TestCase {
public void testLogin() throws Exception {
DaoFactory.load(new DaoFactory(new UserDao() {
public User load(String username) {
User u = new User(username);
u.setPassword("chen");
return u;
}
}));
action.setUsername("chen56");
action.setPassword("chen");
assertEquals("正确登陆", Action.SUCCESS, action.execute());
}
}
|
当然最后还应该把DaoFactory重构rename为更贴切的名称.
2.setter 注射(Setter Injection):
我们在LoginAction中加入一个新的方法:
public void setUserDao(UserDao userDao){
this.userDao=userDao;
}
|
这样就可以把mock UserDao插入到待测类LoginAction中:
public class LoginActionSetterInjectionTest extends TestCase {
public void testLogin() throws Exception {
LoginAction action = new LoginAction();
action.setUserDao(new UserDao() {
public User load(String username) {
User u = new User(username);
u.setPassword("chen");
return u;
}
});
action.setUsername("chen56");
action.setPassword("chen");
assertEquals("正确登陆", Action.SUCCESS, action.execute());
}
}
|
3.构造器注射(Constructor Injection):在LoginAction加入一个新的构造器:
public LoginAction(UserDao userDao) {
this.userDao = userDao;
}
|
这样也可以把依赖对象传入到被测类LoginAction中:
public class LoginActionConstructorInjectionTest extends TestCase {
public void testLogin() throws Exception {
LoginAction action = new LoginAction(new UserDao() {
public User load(String username) {
User u = new User(username);
u.setPassword("chen");
return u;
}
});
action.setUsername("chen56");
action.setPassword("chen");
assertEquals("正确登陆", Action.SUCCESS, action.execute());
}
}
|
以上实现中:Service Locator方法对源程序基本没有修改,但实际组装UserDao的工作却从原来的DaoFactory中分离了出来,通常情况下,我们会在filter或一个所有Action的基类中用模版方法实现他的组装,比如实际上可能会组装hibernate的一个Session到UserDao中。
后2个实现其实在程序中保留了一处专为测试所用的依赖对象入口,在实际使用中,构造器注射更舒心一些。由于我们知道webwork是用缺省构造器来初始化类的,而我们测试则用带UserDao参数的构造器,所以这是一个单选题,很少会产生误解,并且也更简洁些,类的状态也不会在运行期变化。
注:事实上遵守Kent beck的教诲,LoginActionTest是先于LoginAction开发出来的。
|