考虑在你的程序中集成 picocontainer或spring框架
from: http://www.huihoo.com/column/~chen56/webwork-pico/
作者:陈鹏[url=mailto:chen56@msn.com]chen56@msn.com[/url]
实现和接口分离,使用和组装分离是一个基本的对象设计原则,简单的工厂方法(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开发出来的。
第2部分 集成PicoContainer来更完美的消除对象依赖。
ok,前面的方法可以使单元测试更容易些,下面来看看更酷的:集成pico让程序更简洁。
picoContainer为何物?大家可以google上找一下,连接很多。
我们只说明一下它在我们的程序中的作用并用下面的代码来展现它的可爱之处:
集成pico之后的webwork与上一部分的图示只有一点点不同,就是在实例化Action时,它会查找注册到picoContainer本身的组件,也就是注册到pico中的UserDao,并根据匹配的构造器初始化Action类,即执行new LoginAction(userDao)然后调用Action.execute().就这一点点的不同,让我们看看对我们的构造器注射方式的代码产生了些啥变化。
LoginAction 类:去掉了缺省的构造器。
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 userDao) {
//依赖对象在外部初始化
this.userDao = userDao;
}//为了程序的简单,假设用户总是已经注册过的
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;
}
}
测试类:没有变化
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());
}
}
UserDao总有被初始化的时候,ok,现在我们把初始化工作集中在一个WebContainerComposer中,并且在web.xml中用context-param描述它,这样,pico就可以根据pico容器中相应的UserDao组件初始化UserAction类了.
package ftsmen.web.action;
import org.picocontainer.MutablePicoContainer;
import org.picoextras.integrationkit.ContainerComposer;import ftsmen.dao.UserDao;
import ftsmen.dao.HibernateUserDao;
import ftsmen.db.Database;public class WebContainerComposer implements ContainerComposer {
String _nameStr;
public void composeContainer(MutablePicoContainer container, Object name) {
_nameStr = name.toString().toLowerCase();
if (isScope("application")) {
//可以把scope为application的组件注册到这里
} else if (isScope("session")) {
//可以把scope为session的组件注册到这里
} else if (isScope("request")) {
//可以把scope为request的组件注册到这里
//真实程序中Hibernate的Dao实现用Session来初始化
container.registerComponentInstance(
new HibernateUserDao(Database.currentSession()));
/*jdbc实现可能象这样
container.registerComponentInstance(
new JdbcUserDao(Database.connection()));
*/
}
}
private boolean isScope(String scope) {
return _nameStr.indexOf(scope) != -1;
}
}
可以看到,集成pico后的源代码更洁净了,并且Factory的依赖也消除了。
还有什么比清洁溜溜的代码更说明问题呢?
具体集成pico和webwork的方法可以参照
http://www.opensymphony.com/webwork/cookbook/PicoContainer_Integration.html
但其中的WebContainerAssembler类的实现现在已经有所变化,在2004年2月2日左右的实现已经变为我给出的写法。
以上实现并不表明pico只支持构造器注射,实际上目前2个主要的框架spring和pico都支持构造器和setter注射。
总结:用pico或spring这样的东东可以迅速的提高程序依赖方面的质量,你不想试一下吗?
资源:
这里给出一个已经集成过pico-1.0beta5的webwork1.4的例子下载
Inversion of Control Containers and the Dependency Injection pattern
Martin Fowler文章
Oh no, we're testing the Mock!
一篇很好的关于用Mock对象测试的文章,还好,我不是其中的反例