http://www.huihoo.com/column/~chen56/webwork-pico/
實現和接口分離,使用和組裝分離是一個基本的對象設計原則,簡單的工廠方法(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開發出來的。
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這樣的東東可以迅速的提高程序依賴方面的質量,你不想試一下嗎?
資源:
關于作者:
陳鵬,狂熱的程序員 email chen56@msn.com