2 Jive與設計模式
Jive論壇系統使用大量設計模式巧妙地實現了一系列功能。因為設計模式的通用性和可理解性,將幫助更多人很快地理解 Jive論壇源碼,從而可以依據一種“協定”來動態地擴展它。那么使用設計模式還有哪些好處?
2.1 設計模式
設計模式是一套被反復使用、多數人知曉的、經過分類編目的、代碼設計經驗的總結。使用設計模式是為了可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。毫無疑問,設計模式于己于他人于系統都是多贏的。設計模式使代碼編制真正工程化,設計模式是軟件工程的基石。
GOF(設計模式作者簡稱)《設計模式》這本書第一次將設計模式提升到理論高度,并將之規范化,該書提出了23種基本設計模式。自此,在可復用面向對象軟件的發展過程中,新的大量的設計模式不斷出現。
很多人都知道Java是完全面向對象的設計和編程語言,但是由于接受教育以及經驗的原因,大多數程序員或設計人員都是從傳統的過程語言轉變而來,因此在思維習慣上要完全轉變為面向對象的設計和開發方式是困難的,而學習設計模式可以更好地幫助和堅固這種轉變。
凡是學習完成設計模式的人都有一種類似重生的感覺,這種重生可以從很多方面去解釋。換一種新的角度來看待和解決問題應該是一種比較貼切的解釋,而這種新的思維角度培養屬于基礎培訓,因此,設計模式是學習Java的必讀基礎課程之一。
由于設計模式概念比較抽象,對于初學者學習有一定的難度,因此結合Jive論壇系統學習設計模式將是一種很好的選擇。
掌握了設計模式,將會幫助程序員或設計人員以更加可重用性、可伸縮性的眼光來開發應用系統,甚至開發通用的框架系統。框架系統是構成一類特定軟件可復用設計的一組相互協作的類,主要是對應用系統中反復重用部分的提煉,類似一種模板,這是一種結構性的模板。
框架通常定義了應用體系的整體結構、類和對象的關系等設計參數,以便于具體應用實現者能集中精力于應用本身的特定細節。框架強調設計復用,而設計模式最小的可重用單位,因此框架不可避免地會反復使用到設計模式。關于通用框架系統的設計開發將在以后章節中討論。
其實Jive論壇本身也形成了一個基于Web結構的通用框架系統,因為它很多設計思想是可以重用的,例如設定 一個總體入口,通過入口檢查用戶的訪問控制權限,當然還有其他各方面的功能實現方式都是值得在其他系統中借鑒的,也正因為它以模式的形式表現出來,這種可 重用性和可借鑒性就更強。
2.2 ForumFactory與工廠模式
工廠模式是GOF設計模式的主要常用模式,它主要是為創建對象提供了一種接口,工廠模式主要是封裝了創建對象的細節過程,從而使得外界調用一個對象時,根本無需關心這個對象是如何產生的。
在GOF設計模式中,工廠模式分為工廠方法模式和抽象工廠模式。兩者主要區別是,工廠方法是創建一種產品接口下的產品對象,而抽象工廠模式是創建多種產品接口下的產品對象,非常類似Builder生成器模式。在平時實踐中,使用較多的基本是工廠方法模式。
以類SampleOne為例,要創建SampleOne的對象實例:
SampleOne sampleOne = new SampleOne();
如果Sample類有幾個相近的類:SampleTwo或SampleThree,那么創建它們的實例分別是:
SampleTwo sampleTwo = new SampleTwo();
SampleThree sampleThree = new SampleThree();
其實這3個類都有一些共同的特征,如網上商店中銷售書籍、玩具或者化妝品。雖然它們是不同的具體產品,但是它 們有一個共同特征,可以抽象為“商品”。日常生活中很多東西都可以這樣高度抽象成一種接口形式。上面這3個類如果可以抽象為一個統一接口 SampleIF,那么上面語句就可以成為:
SampleIF sampleOne = new SampleOne();
SampleIF sampleTwo = new SampleTwo();
SampleIF sampleThree = new SampleThree();
在實際情況中,有時并不需要同時生成3種對象,而是根據情況在3者之中選一個。在這種情況下,需要使用工廠方法來完成了,創建一個叫SampleFactory的抽象類:
public class SampleFactory{
public abstract SampleIF creator();
}
在這個抽象工廠類中有一個抽象方法creator,但是沒有具體實現,而是延遲到它的子類中實現,創建子類SampleFactoryImp:
public class SampleFactoryImp extends SampleFactory{
public SampleIF creator(){
//根據其他因素綜合判斷返回具體產品
//假設應該返回SampleOne對象
return new SampleOne();
}
}
在SampleFactoryImp中根據具體情況來選擇返回SampleOne、SampleTwo或SampleThree。所謂具體情況有很多種:上下文其他過程計算結果;直接根據配置文件中配置。
上述工廠方法模式中涉及到一個抽象產品接口Sample,如果還有其他完全不同的產品接口,如Product 等,一個子類SampleFactoryImp只能實現一套系列產品方案的生產,如果還需要另外一套系統產品方案,就可能需要另外一個子類 SampleFactoryImpTwo來實現。這樣,多個產品系列、多個工廠方法就形成了抽象工廠模式。
前面已經討論在Jive中設置了論壇統一入口,這個統一入口就是ForumFactory,以下是ForumFactory的主要代碼:
public abstract class ForumFactory {
private static Object initLock = new Object();
private static String className = " com.Yasna.forum.database.DbForumFactory";
private static ForumFactory factory = null;
public static ForumFactory getInstance(Authorization authorization) {
if (authorization == null) {
return null;
}
//以下使用了Singleton 單態模式,將在2.3節討論
if (factory == null) {
synchronized(initLock) {
if (factory == null) {
... //從配置文件中獲得當前className
try {
//動態裝載類
Class c = Class.forName(className);
factory = (ForumFactory)c.newInstance();
}
catch (Exception e) {
return null;
}
}
}
}
//返回 proxy.用來限制授權對forum的訪問
return new ForumFactoryProxy(authorization, factory,factory.getPermissions(authorization));
}
//創鍵產品接口Forum的具體對象實例
public abstract Forum createForum(String name, String description)
throws UnauthorizedException, ForumAlreadyExistsException;
//創鍵產品接口ForumThread的具體對象實例
public abstract ForumThread createThread(ForumMessage rootMessage)
throws UnauthorizedException;
//創鍵產品接口ForumMessage的具體對象實例
public abstract ForumMessage createMessage();
....
}
ForumFactory中提供了很多抽象方法如createForum、createThread和 createMessage()等,它們是創建各自產品接口下的具體對象,這3個接口就是前面分析的基本業務對象Forum、ForumThread和 ForumMessage,這些創建方法在ForumFactory中卻不立即執行,而是推遲到ForumFactory子類中實現。
ForumFactory的子類實現是 com.Yasna.forum.database.DbForumFactory,這是一種數據庫實現方式。即在DbForumFactory中分別實 現了在數據庫中createForum、createThread和createMessage()等3種方法,當然也提供了動態擴展到另外一套系列產品 的生產方案的可能。如果使用XML來實現,那么可以編制一個XmlForumFactory的具體工廠子類來分別實現3種創建方法。
因此,Jive論壇在統一入口處使用了抽象工廠模式來動態地創建論壇中所需要的各種產品,如圖3-4所示。
圖3-4 ForumFactory抽象工廠模式圖
圖3-4中,XmlForumFactory和DbForumFactory作為抽象工廠 ForumFactory的兩個具體實現,而Forum、ForumThread和ForumMessage分別作為3個系列抽象產品接口,依靠不同的工 廠實現方式,會產生不同的產品對象。
從抽象工廠模式去理解Jive論壇統一入口處,可以一步到位掌握了幾個類之間的大概關系。因為使用了抽象工廠模式這種通用的設計模式,可以方便源碼閱讀者快速地掌握整個系統的結構和來龍去脈,圖3-4這張圖已經初步展示了Jive的主要框架結構。
細心的讀者也許會發現,在上面ForumFactory有一個getInstance比較令人費解,這將在2.3節進行討論。
2.3 統一入口與單態模式
在上面ForumFactory的getInstance方法使用單態(SingleTon)模式。單態模式是保證一個類有且僅有一個對象實例,并提供一個訪問它的全局訪問點。
前面曾提到ForumFactory是Jive提供客戶端訪問數據庫系統的統一入口。為了保證所有的客戶端請求都要經過這個ForumFactory,如果不使用單態模式,客戶端下列調用語句表示生成了ForumFactory實例:
ForumFactory factory = new DbForumFactory();
客戶端每發生一次請求都調用這條語句,這就會發生每次都生成不同factory對象實例,這顯然不符合設計要求,因此必須使用單態模式。
一般在Java實現單態模式有幾種選擇,最常用而且安全的用法如下:
public class Singleton {
private Singleton(){}
//在自己內部定義自己一個實例,是不是很奇怪
//注意這是private,只供內部調用
private static Singleton instance = new Singleton();
//這里提供了一個供外部訪問本class的靜態方法,可以直接訪問
public static Singleton getInstance() {
return instance;
}
}
單態模式一共使用了兩條語句實現:第一條直接生成自己的對象,第二條提供一個方法供外部調用這個對象,同時最好將構造函數設置為private,以防止其他程序員直接使用new Singleton生成實例。
還有一種Java單態模式實現:
public class Singleton {
private Singleton(){}
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance==null)
instance=new Singleton()
return instance;
}
}
在上面代碼中,使用了判斷語句。如果instance為空,再進行實例化,這成為lazy initialization。注意getInstance()方法的synchronized,這個synchronized很重要。如果沒有 synchronized,那么使用getInstance()在第一次被訪問時有可能得到多個Singleton實例。
關于lazy initialization的Singleton有很多涉及double-checked locking (DCL)的討論,有興趣者可以進一步研究。一般認為第一種形式要更加安全些;但是后者可以用在類初始化時需要參數輸入的情況下。
在Jive的ForumFactory中采取了后者lazy initialization形式,這是為了能夠動態配置指定ForumFactory的具體子類。在getInstance中,從配置文件中獲得當前工 廠的具體實現,如果需要啟動XmlForumFactory,就不必修改ForumFactory代碼,直接在配置文件中指定className的名字為 XmlForumFactory。這樣通過下列動態裝載機制生成ForumFactory具體對象:
Class c = Class.forName(className);
factory = (ForumFactory)c.newInstance();
這是利用Java的反射機制,可以通過動態指定className的數值而達到生成對象的方式。
使用單態模式的目標是為了控制對象的創建,單態模式經常使用在控制資源的訪問上。例如數據庫連接或 Socket連接等。單態模式可以控制在某個時刻只有一個線程訪問資源。由于Java中沒有全局變量的概念,因此使用單態模式有時可以起到這種作用,當然 需要注意是在一個JVM中。
2.4 訪問控制與代理模式
仔細研究會發現,在ForumFactory的getInstance方法中最后的返回值有些奇怪。按照單態 模式的概念應該直接返回factory這個對象實例,但是卻返回了ForumFactoryProxy的一個實例,這實際上改變了單態模式的初衷。這樣客 戶端每次通過調用ForumFactory的getInstance返回的就不是ForumFactory的惟一實例,而是新的對象。之所以這樣做是為了 訪問權限的控制,姑且不論這樣做的優劣,先看看什么是代理模式。
代理模式是屬于設計模式結構型模式中一種,它是實際訪問對象的代理對象,或者影子對象,主要達到控制實際對象的訪問。這種控制的目的很多,例如提高性能等。即遠程代理模式,這種模式將在以后章節討論。
其中一個主要的控制目的是控制客戶端對實際對象的訪問權限。在Jive系統中,因為有角色權限的分別,對于Forum、ForumThread和FroumMessage的訪問操作必須經過權限機制驗證后才能進行。
以ForumFactoryProxy中的createForum方法為例,其實ForumFactoryProxy也是FroumFactory的一種工廠實現,它的createForum具體實現如下:
public Forum createForum(String name, String description)
throws UnauthorizedException, ForumAlreadyExistsException
{
if (permissions.get(ForumPermissions.SYSTEM_ADMIN)) {
Forum newForum = factory.createForum(name, description);
return new ForumProxy(newForum, authorization, permissions);
}
else {
throw new UnauthorizedException();
}
}
在這個方法中進行了權限驗證,判斷是否屬于系統管理員。如果是,將直接從DbForumFactory對象 factory的方法createForum中獲得一個新的Forum對象,然后再返回Forum的子類代理對象ForumProxy。因為在Forum 中也還有很多屬性和操作方法,這些也需要進行權限驗證。ForumProxy和ForumFactoryProxy起到類似的作用。
Jive中有下列幾個代理類:
· ForumFactoryProxy:客戶端和DbForumFactory之間的代理。客戶端訪問DbForumFactory的任何方法都要先經過ForumFactoryProxy相應方法代理一次。以下意思相同。
· ForumProxy:客戶端和DbForum之間的代理,研究Forum對象的每個方法,必須先看ForumProxy對象的方法。
· ForumMessageProxy:客戶端和DbForumMessage之間的代理。
· ForumThreadProxy:客戶端和DbForumThread之間的代理。
User和Group也有相應的代理類。
由以上分析看出,每個數據對象都有一個代理。如果系統中數據對象非常多,依據這種一對一的代理關系,會有很多代理類,將使系統變得不是非常干凈,因此可以使用動態代理來代替這所有的代理類,具體實現將在以后章節討論。
2.5 批量分頁查詢與迭代模式
迭代(Iterator)模式是提供一種順序訪問某個集合各個元素的方法,確保不暴露該集合的內部表現。迭代模式應用于對大量數據的訪問,Java Collection API中Iterator就是迭代模式的一種實現。
在前面章節已經討論過,用戶查詢大量數據,從數據庫不應該直接返回ResultSet,應該是 Collection。但是有一個問題,如果這個數據很大,需要分頁面顯示。如果一下子將所有頁面要顯示的數據都查詢出來放在Collection,會影 響性能。而使用迭代模式則不必將全部集合都展現出來,只有遍歷到某個元素時才會查詢數據庫獲得這個元素的數據。
以論壇中顯示帖子主題為例,在一個頁面中不可能顯示所有主題,只有分頁面顯示,如圖3-5所示。
圖3-5中一共分15頁來顯示所有論壇帖子,可以從顯示Forum.jsp中發現下列語句可以完成上述結果:
ResultFilter filter = new ResultFilter(); //設置結果過濾器
filter.setStartIndex(start); //設置開始點
filter.setNumResults(range); //設置范圍
ForumThreadIterator threads = forum.threads(filter); //獲得迭代器
while(threads.hasNext){
//逐個顯示threads中帖子主題,輸出圖3-5中的每一行
}
圖3-5 分頁顯示所有帖子
上述代碼中主要是從Forum的threads方法獲得迭代器ForumThreadIterator的實 例,依據前面代理模式中分析、研究Forum對象的方法,首先是看ForumProxy中對應方法,然后再看DbForum中對應方法的具體實現。在 ForumProxy中,threads方法如下:
public ForumThreadIterator threads(ResultFilter resultFilter) {
ForumThreadIterator iterator = forum.threads(resultFilter);
return new ForumThreadIteratorProxy(iterator, authorization, permissions);
}
首先是調用了DbForum中具體的threads方法,再追蹤到DbForum中看看,它的threads方法代碼如下:
public ForumThreadIterator threads(ResultFilter resultFilter) {
//按resultFilter設置范圍要求獲得SQL查詢語句
String query = getThreadListSQL(resultFilter, false);
//獲得resultFilter設置范圍內的所有ThreadID集合
long [] threadBlock = getThreadBlock(query.toString(), resultFilter.getStartIndex());
//以下是計算查詢區域的開始點和終點
int startIndex = resultFilter.getStartIndex();
int endIndex;
// If number of results is set to inifinite, set endIndex to the total
// number of threads in the forum.
if (resultFilter.getNumResults() == ResultFilter.NULL_INT) {
endIndex = (int)getThreadCount(resultFilter);
}else {
endIndex = resultFilter.getNumResults() + startIndex;
}
return new ForumThreadBlockIterator(threadBlock, query.toString(),
startIndex, endIndex, this.id, factory);
}
ResultFilter是一個查詢結果類,可以對論壇主題Thread和帖子內容Message進行過濾或 排序,這樣就可以根據用戶要求定制特殊的查詢范圍。如查詢某個用戶去年在這個論壇發表的所有帖子,那只要創建一個ResultFilter對象就可以代表 這個查詢要求。
在上面threads方法代碼中,第一步是先定制出相應的動態SQL查詢語句,然后使用這個查詢語句查詢數據庫,獲得查詢范圍內所有的ForumThread的ID集合,然后在這個ID集合中獲得當前頁面的ID子集合,這是非常關鍵的一步。
在這關鍵的一步中,有兩個重要的方法getThreadListSQL和getThreadBlock:
· GetThreadListSQL:獲得SQL查詢語句query的值,這個方法Jive實現起來顯得非常地瑣碎。
· GetThreadBlock:獲得當前頁面的ID子集合,那么如何確定ID子集合的開始位置呢?查看getThreadBlock方法代碼,可以發現,它是使用最普遍的ResultSet next()方法來逐個跳躍到開始位置。
上面代碼的Threads方法中最后返回的是ForumThreadBlockIterator,它是抽象類 ForumThreadIterator的子類,而ForumThreadIterator繼承了Collection的Iterator,以此聲明自己 是一個迭代器,ForumMessageBlockIterator實現的具體方法如下:
public boolean hasNext(); //判斷是否有下一個元素
public boolean hasPrevious() //判斷是否有前一個元素
public Object next() throws java.util.NoSuchElementException //獲得下一個元素實例
ForumThreadBlockIterator中的Block是“頁”的意思,它的一個主要類變量 threadBlock包含的是一個頁面中所有ForumThread的ID,next()方法實際是對threadBlock中ForumThread 進行遍歷,如果這個頁面全部遍歷完成,將再獲取下一頁(Block)數據。
在ForumThreadBlockIterator重要方法getElement中實現了兩個功能:
· 如果當前遍歷指針超過當前頁面,將使用getThreadBlock獲得下一個頁面的ID子集合;
· 如果當前遍歷指針在當前頁面之內,根據ID獲得完整的數據對象,實現輸出;
ForumThreadBlockIterator的getElement方法代碼如下:
private Object getElement(int index) {
if (index < 0) { return null; }
// 檢查所要獲得的 element 是否在本查詢范圍內(當前頁面內)
if (index < blockStart ||
index >= blockStart + DbForum.THREAD_BLOCK_SIZE) {
try {
//從緩沖中獲得Forum實例
DbForum forum = factory.cacheManager.forumCache.get(forumID);
//獲得下一頁的內容
this.threadBlock = forum.getThreadBlock(query, index);
this.blockID = index / DbForum.THREAD_BLOCK_SIZE;
this.blockStart = blockID * DbForum.THREAD_BLOCK_SIZE;
} catch (ForumNotFoundException fnfe) {
return null;
}
}
Object element = null;
// 計算這個元素在當前查詢范圍內的相對位置
int relativeIndex = index % DbForum.THREAD_BLOCK_SIZE;
// Make sure index isn't too large
if (relativeIndex < threadBlock.length) {
try {
// 從緩沖中獲得實際thread 對象
element = factory.cacheManager.threadCache.get(
threadBlock[relativeIndex]);
} catch (ForumThreadNotFoundException tnfe) { }
}
return element;
}
ForumThreadBlockIterator是真正實現分頁查詢的核心功能, ForumThreadBlockIterator對象返回到客戶端的過程中,遭遇ForumThreadIteratorProxy的截獲,可以回頭看 看ForumProxy中的threads方法,它最終返回給調用客戶端Forum.jsp的是ForumThreadIteratorProxy實例。
ForumThreadIteratorProxy也是迭代器ForumThreadIterator的一個子類,它的一個具體方法中:
public Object next() {
return new ForumThreadProxy((ForumThread)iterator.next(), authorization,
permissions);
}
這一句是返回一個ForumThreadProxy實例,返回就是一個ForumThread實例的代理。這里,Jive使用代理模式實現訪問控制實現得不是很巧妙,似乎有代理到處“飛”的感覺,這是可以對之進行改造的。
從以上可以看出,Jive在輸出如圖3-5所示的多頁查詢結果時,采取了下列步驟:
(1)先查詢出符合查詢條件的所有對象元素的ID集合,注意不是所有對象元素,只是其ID的集合,這樣節約了大量內存。
(2)每個頁面視為一個Block,每當進入下一頁時,獲得下一個頁面的所有對象的ID集合。
(3)輸出當前頁面的所有對象時,首先從緩沖中獲取,如果緩沖中沒有,再根據ID從數據庫中獲取完整的對象數據。
上述實現方法完全基于即查即顯,相比于一般批量查詢做法:一次性獲得所有數據,然后遍歷數據結果集ResultSet,Jive這種批量查詢方式是一種比較理想的選擇。
以上是ForumThread的批量顯示,有關帖子內容ForumMessage也是采取類似做法。在每個 ForumThread中可能有很多帖子內容(ForumMessage對象集合),也不能在一個頁面中全部顯示,所以也是使用迭代模式來實現的。顯示一 個Forum主題下所有帖子內容的功能由ForumThread的messages()方法完成,檢查它的代理類FroumThreadProxy如何具 體完成:
public Iterator messages(ResultFilter resultFilter) {
Iterator iterator = thread.messages(resultFilter);
return new IteratorProxy(JiveGlobals.MESSAGE, iterator, authorization, permissions);
}
實現的原理基本相同,返回的都是一個Iterator代理類,在這些代理類中都是進行用戶權限檢驗的。
Jive中也有關于一次性獲得所有數據,然后遍歷ResultSet的做法。這種做法主要適合一次性查詢數據庫的所有數據,例如查詢當前所有論壇Forum,首先實現SQL語句:
SELECT forumID FROM jiveForum
獲得所有Forum的forumID,這段代碼位于DbForumFactory.java的forums方法中,如下:
public Iterator forums() {
if (forums == null) {
LongList forumList = new LongList();
Connection con = null;
PreparedStatement pstmt = null;
try {
con = ConnectionManager.getConnection();
// GET_FORUMS值是SELECT forumID FROM jiveForum
pstmt = con.prepareStatement(GET_FORUMS);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
forumList.add(rs.getLong(1)); //將所有查詢ID結果放入forumList中
}
}catch (SQLException sqle) {
sqle.printStackTrace();
} finally {
…
}
return new DatabaseObjectIterator(JiveGlobals.FORUM, forums, this);
}
forums方法是返回一個DatabaseObjectIterator,這個 DatabaseObjectIterator也是一個迭代器,但是實現原理要比ForumThreadBlockIterator簡單。它只提供了一個 遍歷指針,在所有ID結果集中遍歷,然后也是通過ID獲得完整的數據對象。
總之,Jive中關于批量查詢有兩種實現方式:以ForumThreadBlockIterator為代表的實現方式適合在數據量巨大、需要多頁查詢時使用;而DatabaseObjectIterator則是推薦在一個頁面中顯示少量數據時使用。
2.6 過濾器與裝飾模式
裝飾(Decorator)模式是動態給一個對象添加一些額外的職責,或者說改變這個對象的一些行為。這就類似于使用油漆為某個東西刷上油漆,在原來的對象表面增加了一層外衣。
在裝飾模式中,有兩個主要角色:一個是被刷油漆的對象(decoratee);另外一個是給decoratee刷油漆的對象(decorator)。這兩個對象都繼承同一個接口。
首先舉一個簡單例子來說明什么是裝飾模式。
先創建一個接口:
public interface Work
{
public void insert();
}
這是一種打樁工作的抽象接口,動作insert表示插入,那么插入什么?下面這個實現表示方形木樁的插入:
public class SquarePeg implements Work{
public void insert(){
System.out.println("方形樁插入");
}
}
本來這樣也許就可以滿足打樁的工作需要,但是有可能土質很硬,在插入方形樁之前先要打一個洞,那么又將如何實現?可以編制一個Decorator類,同樣繼承Work接口,但是在實現insert方法時有些特別:
public class Decorator implements Work{
private Work work;
//額外增加的功能被打包在這個List中
private ArrayList others = new ArrayList();
public Decorator(Work work)
{
this.work=work;
others.add("打洞"); //準備好額外的功能
}
public void insert(){
otherMethod();
work.insert();
}
public void otherMethod()
{
ListIterator listIterator = others.listIterator();
while (listIterator.hasNext())
{
System.out.println(((String)(listIterator.next())) + " 正在進行");
}
}
}
在Decorator的方法insert中先執行otherMethod()方法,然后才實現SquarePeg的insert方法。油漆工Decorator給被油漆者SquarePeg添加了新的行為——打洞。具體客戶端調用如下:
Work squarePeg = new SquarePeg();
Work decorator = new Decorator(squarePeg);
decorator.insert();
本例中只添加了一個新的行為(打洞),如果還有很多類似的行為,那么使用裝飾模式的優點就體現出來了。因為可以通過另外一個角度(如組織新的油漆工實現子類)來對這些行為進行混合和匹配,這樣就不必為每個行為創建一個類,從而減少了系統的復雜性。
使用裝飾模式可以避免在被油漆對象decoratee中包裝很多動態的,可能需要也可能不需要的功能,只要在系統真正運行時,通過油漆工decorator來檢查那些需要加載的功能,實行動態加載。
Jive論壇實現了信息過濾功能。例如可以將帖子內容中的HTML語句過濾掉;可以將帖子內容中Java代碼 以特別格式顯示等。這些過濾功能有很多,在實際使用時不一定都需要,是由實際情況選擇的。例如有的論壇就不需要將帖子內容的HTML語句過濾掉,選擇哪些 過濾功能是由論壇管理者具體動態決定的。而且新的過濾功能可能隨時可以定制開發出來,如果試圖強行建立一種接口包含所有過濾行為,那么到時有新過濾功能加 入時,還需要改變接口代碼,真是一種危險的行為。
裝飾模式可以解決這種運行時需要動態增加功能的問題,且看看Jive是如何實現的。
前面討論過,在Jive中,有主要幾個對象ForumFactory、Forum以及ForumThread 和ForumMessage,它們之間的關系如圖3-2所示。因此帖子內容ForumMessage對象的獲得是從其上級FroumThread的方法 getMessage中獲取,但是在實際代碼中,ForumThread的方法getMessage委托ForumFactory來獲取 ForumMessage對象。看看ForumThread的子類DbForumThread的getMessage代碼:
public ForumMessage getMessage(long messageID)
throws ForumMessageNotFoundException
{
return factory.getMessage(messageID, this.id, forumID);
}
這是一種奇怪的委托,大概是因為需要考慮到過濾器功能有意為之吧。那就看看ForumFactory的具體實 現子類DbForumFactory的getMessage功能,getMessage是將數據庫中的ForumMessage對象經由過濾器過濾一遍后 輸出(注:因為原來的Jive的getMessage代碼考慮到可緩存或不可緩存的過濾,比較復雜,實際過濾功能都是可以緩存的,因此精簡如下)。
protected ForumMessage getMessage(long messageID, long threadID, long forumID)
throws ForumMessageNotFoundException
{
DbForumMessage message = cacheManager.messageCache.get(messageID);
// Do a security check to make sure the message comes from the thread.
if (message.threadID != threadID) {
throw new ForumMessageNotFoundException();
}
ForumMessage filterMessage = null;
try {
// 應用全局過濾器
filterMessage = filterManager.applyFilters(message);
Forum forum = getForum(forumID);
//應用本論壇過濾器
filterMessage = forum.getFilterManager().applyFilters(filterMessage);
}
catch (Exception e) { }
return filterMessage;
}
上面代碼實際是裝飾模式的客戶端調用代碼,DbForumMessage 的實例message是被油漆者decoratee。通過filterManager 或forum.getFilterManager()的applyFilter方法,將message實行了所有的過濾功能。這就類似前面示例的下列語 句:
Work decorator = new Decorator(squarePeg);
forum.getFilterManager()是從數據庫中獲取當前配置的所有過濾器類。每個Forum都有一套自己的過濾器類,這是通過下列語句實現的:
FilterManager filterManager = new DbFilterManager();
在DbFilterManager 的類變量ForumMessageFilter [] filters中保存著所有的過濾器,applyFilters方法實行過濾如下:
public ForumMessage applyFilters(ForumMessage message) {
for (int i=0; i < filters.length; i++) {
if (filters[i] != null) {
message = filters[i].clone(message);
}
}
return message;
}
而ForumMessageFilter是ForumMessage的另外一個子類,被油漆者DbForumMessage通過油漆工ForumMessageFilter增加了一些新的行為和功能(過濾),如圖3-6所示。
圖3-6 裝飾模式
這就組成了一個稍微復雜一點的裝飾模式。HTMLFilter實現了HTML代碼過濾功能,而JavaCodeHighLighter實現了Java代碼過濾功能,HTMLFilter代碼如下:
public class HTMLFilter extends ForumMessageFilter {
public ForumMessageFilter clone(ForumMessage message){
HTMLFilter filter = new HTMLFilter();
filter.message = message;
return filter;
}
public boolean isCacheable() {
return true;
}
public String getSubject() {
return StringUtils.escapeHTMLTags(message.getSubject());
}
public String getBody() {
return StringUtils.escapeHTMLTags(message.getBody());
}
}
HTMLFilter中重載了ForumMessage的getSubject()、getBody()方法,實際是改變了這兩個原來的行為,這類似前面舉例的方法:
public void insert(){
otherMethod();
work.insert();
}
這兩者都改變了被油漆者的行為。
在HTMLFilter中還使用了原型(Prototype)模式,原型模式定義是:用原型實例指定創建對象的種類,并且通過復制這些原型創建新的對象。按照這種定義,Java的clone技術應該是原型模式的一個實現。
HTMLFilter的clone方法實際就是在當前HTMLFilter實例中再生成一個同樣的實例。這樣 在處理多個并發請求時,不用通過同一個過濾器實例進行處理,提高了性能。但是HTMLFilter的clone方法是采取new方法來實現,不如直接使用 Object的native方法速度快。
因為在DbFilterManager中是根據配置使用類反射機制動態分別生成包括HTMLFilter在內的過濾器實例。但是每種過濾器實例只有一個,為了使得大量用戶不必爭奪一個過濾器實例來實現過濾,就采取了克隆方式,這種實戰手法可以借鑒在自己的應用系統中。
2.7 主題監測與觀察者模式
觀察者(Observer)模式是定義對象之間一對多的依賴關系,當一個被觀察的對象發生改變時,所有依賴于它的對象都會得到通知并采取相應行為。
使用觀察者模式的優點是將被觀察者和觀察者解耦,從而可以不影響被觀察者繼續自己的行為動作。觀察者模式適合應用于一些“事件觸發”場合。
在Jive中,用戶也許會對某個主題感興趣,希望關于此主題發生的任何新的討論能通過電子郵件通知他,因此他訂閱監視了這個主題。因為這個功能的實現會引入電子郵件的發送。在前面章節已經討論了電子郵件發送有可能因為網絡原因延遲,如果在有人回復這個主題時,立即進行電子郵件發送,通知所有訂閱該主題的用戶。那么該用戶可能等待很長時間得不到正常回應。
使用觀察者模式,可以通過觸發一個觀察者,由觀察者通過另外線程來實施郵件發送,而被觀察者發出觸發通知后,可以繼續自己原來的邏輯行為。
看看Jive的WatchManager類:
public interface WatchManager {
//正常監察類型,用戶在這個主題更新后再次訪問時,會明顯地發現
public static final int NORMAL_WATCH = 0;
// 當主題變化時,通過電子郵件通知用戶
public static final int EMAIL_NOTIFY_WATCH = 1;
//設置一個主題被觀察的時間,默認為30天
public void setDeleteDays(int deleteDays) throws UnauthorizedException;
public int getDeleteDays();
//是否激活了E-mail提醒
public boolean isEmailNotifyEnabled() throws UnauthorizedException;
public void setEmailNotifyEnabled(boolean enabled) throws UnauthorizedException;
//保存E-mail的內容
public String getEmailBody() throws UnauthorizedException;
public void setEmailBody(String body) throws UnauthorizedException;
//保存E-mail的主題
public String getEmailSubject() throws UnauthorizedException;
public void setEmailSubject(String subject) throws UnauthorizedException;
…
//為某個主題創建一個觀察者
public void createWatch(User user, ForumThread thread, int watchType)
throws UnauthorizedException;
//刪除某個主題的觀察者
public void deleteWatch(User user, ForumThread thread, int watchType)
//得到一個主題的所有觀察者
public Iterator getWatchedForumThreads(User user, int watchType)
throws UnauthorizedException;
//判斷一個用戶是否在觀察監視該主題
public boolean isWatchedThread(User user, ForumThread thread, int watchType)
throws UnauthorizedException;
…
}
DbWatchManager是WatchManager的一個子類,通過數據庫保存著有關某個主題被哪些用戶監視等數據資料。WatchManager對象是隨同DbForumFactory()一起生成的。
在DbWatchManager中有一個WatchManager沒有的很重要的方法——通知方法:
protected void notifyWatches(ForumThread thread) {
//If watches are turned on.
if (!emailNotifyEnabled) {
return;
}
//通知所有觀察這個主題的用戶
EmailWatchUpdateTask task = new EmailWatchUpdateTask(this, factory, thread);
TaskEngine.addTask(task);
}
這個方法用來觸發所有有關這個主題的監視或訂閱用戶,以E-mail發送提醒他們。那么這個通知方法本身又是如何被觸發的?從功能上分析,應該是在發表新帖子時觸發。
在DbForumThread的addMessage的最后一行有一句:
factory.watchManager.notifyWatches(this);
這其實是調用了DbWatchManager的notifyWatches方法,因此確實是在增加新帖子時觸發了該帖子的所有觀察者。
notifyWatches方法中在執行E-mail通知用戶時,使用了TaskEngine來執行E- mail發送。E-mailWatchUpdateTask是一個線程類,而TaskEngine是線程任務管理器,專門按要求啟動如E- mailWatchUpdateTask這樣的任務線程。其實TaskEngine是一個簡單的線程池,它不斷通過查詢Queue是否有可運行的線程,如 果有就直接運行線程。
public class TaskEngine {
//任務列表
private static LinkedList taskList = null;
//工作數組
private static Thread[] workers = null;
private static Timer taskTimer = null;
private static Object lock = new Object();
static {
//根據配置文件初始化任務啟動時間
taskTimer = new Timer(true);
// 默認使用7個線程來裝載啟動任務
workers = new Thread[7];
taskList = new LinkedList();
for (int i=0; i<workers.length; i++) {
// TaskEngineWorker是個簡單的線程類
TaskEngineWorker worker = new TaskEngineWorker();
workers[i] = new Thread(worker);
workers[i].setDaemon(true);
workers[i].start(); //啟動TaskEngineWorker這個線程
}
}
//TaskEngineWorker內部類
private static class TaskEngineWorker implements Runnable {
private boolean done = false;
public void run() {
while (!done) {
//運行nextTask方法
nextTask().run();
}
}
}
// nextTask()返回的是一個可運行線程,是任務列表Queue的一個讀取者
private static Runnable nextTask() {
synchronized(lock) {
// 如果沒有任務,就鎖定在這里
while (taskList.isEmpty()) {
try {
lock.wait(); //等待解鎖
} catch (InterruptedException ie) { }
}
//從任務列表中取出第一個任務線程
return (Runnable)taskList.removeLast();
}
}
public static void addTask(Runnable r) {
addTask(r, Thread.NORM_PRIORITY);
}
//這是任務列表Queue的生產者
public static void addTask(Runnable task, int priority) {
synchronized(lock) {
taskList.addFirst(task);
//提醒所有鎖在lock這里的線程可以運行了
//這是線程的互相通知機制,可參考線程參考資料
lock.notifyAll();
}
}
…
}
在TaskEngine中啟動設置了一個消息管道Queue和兩個線程。一個線程是負責向Queue里放入 Object,可謂是消息的生產者;而另外一個線程負責從Queue中取出Object,如果Queue中沒有Object,那它就鎖定(Block)在 那里,直到Queue中有Object,因為這些Object本身也是線程,因此它取出后就直接運行它們。
這個TaskEngine建立的模型非常類似JMS(Java消息系統),雖然它們功能類似,但不同的是: JMS是一個分布式消息發布機制,可以在多臺服務器上運行,處理能力要強大得多。而TaskEngine由于基于線程基礎,因此不能跨JVM實現。可以說 TaskEngine是一個微觀組件,而JMS則是一個宏觀架構系統。JMS相關討論將在后面章節進行。
以上討論了Jive系統中觀察者模式的實現,Jive使用線程比較基礎的概念實現了觀察者模式,當然有助于了解J2EE很多底層的基礎知識,整個Web容器的技術實現就是基于線程池原理建立的。
Java的JDK則提供了比較方便的觀察者模式API——java.util.Observable和java.util.Observer,它們的用戶非常簡單,只要被觀察者繼承Observable,然后使用下列語句設置觀察點:
setChanged();
notifyObservers(name); //一旦執行本代碼,就觸發觀察者了
而觀察者只要實現Observer接口,并實現update方法,在update方法中將被觀察者觸發后傳來的object進行處理。舉例如下:
網上商店中商品價格可能發生變化,如果需要在價格變化時,首頁能夠自動顯示這些降價產品,那么使用觀察者模式將方便得多。首先,商品是一個被觀察者:
public class product extends Observable{
private float price;
public float getPrice(){ return price;}
public void setPrice(){
this.price=price;
//商品價格發生變化,觸發觀察者
setChanged();
notifyObservers(new Float(price));
}
...
}
價格觀察者實現observer接口:
public class PriceObserver implements Observer{
private float price=0;
public void update(Observable obj,Object arg){
if (arg instanceof Float){
price=((Float)arg).floatValue();
System.out.println("PriceObserver :price changet to "+price);
}
}
}
這樣,一個簡單的觀察者模式就很容易地實現了。