觀察者模式+中介者模式。
有一個產品,他有多個觸發事件,他產生的時候觸發一個建立事件,修改的時候觸發修改事件,刪除的時候觸發刪除事件,這就類似於我們的文字框,初始化(也就是建立)的時候要觸發一個onLoad或onCreate事件,修改的時候觸發onChange事件,雙擊(類似於刪除)的時候又觸發onDbClick事件,我們今天的目標就是來思考怎麼實現這樣一個架構。
設計都是先易後難,我們先從最簡單的部分入手。首先需要一個產品,並且該產品要有建立、修改、銷燬的動作,很明顯這就是一個工廠方法模式。同時產品也可以通過克隆方式產生,這與我們在GUI設計中經常使用的複製貼上操作相類似,這非常明顯就是原型模式。
我們使用了工廠方法模式建立產品,使用原型模式讓物件可以被拷貝,僅僅這兩個模式還不足以解決我們的問題,想想看,產品的產生是有一定的條件的,不是誰想產生就產生,否則怎麼能夠觸發建立事件呢?因此需要限定產品的建立者,所以我們把產品和工廠的關係定位為組合關係,而不是簡單地聚集或依賴關係。換句話說,產品只能由工廠類建立,而不能被其他物件通過new方式建立,因此我們在這裡還用到一個單來源呼叫(Single Call)方法來解決問題。這是一個方法,不是一個設計模式。
我們先來看產品類的原始碼,他比較簡單,如下所示。
public class Product implements Cloneable { // 產品名稱 private String name; // 是否可以屬性變更 private boolean canChanged = false; /** * 產生一個新的產品 * * @param manager * @param _name */ public Product(ProductManager manager, String _name) { // 允許建立產品 if (manager.isCreateProduct()) { this.canChanged = true; this.name = _name; } } public String getName() { return name; } public void setName(String name) { if (this.canChanged) { this.name = name; } } @Override protected Product clone() { Product p = null; try { p = (Product) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return p; } }
在產品類中,我們只定義產品的一個屬性:產品名稱(name),並實現了getter/setter方法,然後我們實現了他的clone方法,確保物件是可以被拷貝的。還有一個特殊的地方是我們的建構函式,他怎麼會要求傳遞進來一個工廠物件ProductManager呢?保留你的好奇心,馬上為你揭曉答案。我們繼續看程式碼,工廠類如下所示。
public class ProductManager { // 是否可以建立一個產品 private boolean isPermittedCreate = false; /** * 建立一個產品 * * @param name * @return */ public Product createProduct(String name) { // 首先修改許可權,允許建立 this.isPermittedCreate = true; Product p = new Product(this, name); return p; } /** * 廢棄一個產品 * * @param p */ public void abandonProduct(Product p) { // 銷燬一個產品,例如刪除資料庫記錄 p = null; } /** * 修改一個產品 * * @param p * @param name */ public void editProduct(Product p, String name) { // 修改後的產品 p.setName(name); } /** * 獲得是否可以建立一個產品 * * @return */ public boolean isCreateProduct() { return this.isPermittedCreate; } /** * 克隆一個產品 * * @param p * @return */ public Product clone(Product p) { return p.clone(); } }
仔細看看工廠類,產品的建立、修改、遺棄、克隆方法都很簡單,但有一個方法可不簡單——isCreateProduct方法,他的作用是告訴產品類“我是能建立產品的”,注意看我們的程式,在工廠類ProductManager中定義了一個私有變數isCreateProduct,該變數只有在工廠類的createProduct函式中才能設定為true,在建立產品的時候,產品類Product的建構函式要求傳遞工廠物件,然後判斷是否能夠建立產品,即使你想使用類似這樣的方法:
Product p = new Product(new ProductManager(), "abc");
也是不可能創建出產品的,他在產品中限制必須是當前有效工廠才能生產該產品,而且也只有有效地工廠才能修改產品,看看產品類的canChanged屬性,只有他為true時,產品才可以修改,產品就問“你有權力建立我嗎”於是工廠類出示了兩個證明材料證明自己可以建立產品:一是“我是你的工廠類”,二是“我的isCreateProduct返回true,我有權建立”,於是產品就被創建出來了。這種一個物件只能由固定的物件初始化的方法叫做單來源呼叫(Single Call)——很簡單,但非常有用的方法。
注意:採用單來源呼叫的兩個物件一般是組合關係,兩者有相同的生命期,他通常適用於有單例模式和工廠方法模式的場景中。
我們繼續往下分析,一個產品新建就要觸發事件,那事件是什麼?當然也是一個物件了,需要把他設計出來,僅僅有事件還不行,還要考慮有人去處理這個事件,產生了一個事件不可能沒有物件去處理吧?如果是這樣那事件還有什麼意義呢?既然去處理,那就需要一個通知渠道了,於是觀察者模式準備好了。
觀察者為EventDispatch類,他使用了單例模式,避免物件膨脹,但同時也帶來了效能及執行緒安全隱患,這點需要大家在實際應用中注意(想想Spring中的Bean注入,預設也是單例,在通常的應用中一般不需要修改,除非是較大併發的應用)。我們來看程式碼,先來看事件型別定義,他是一個列舉型別,如下所示。
public enum ProductEventType {
// 新建一個產品
NEW_PRODUCT(1),
// 刪除一個產品
DEL_PRODUCT(2),
// 修改一個產品
EDIT_PRODUCT(3),
// 克隆一個產品
CLONE_PRODUCT(4);
private int vlaue = 0;
private ProductEventType(int vlaue) {
this.vlaue = vlaue;
}
public int getVlaue() {
return vlaue;
}
}
這裡定義了4個事件型別,分別是新建、修改、刪除以及克隆,比較簡單。我們再來看產品的事件,如下所示。
public class ProductEvent extends Observable {
// 事件起源
private Product source;
// 事件的型別
private ProductEventType type;
/**
* 傳入事件的源頭,預設為新建型別
*
* @param p
*/
public ProductEvent(Product p) {
this(p, ProductEventType.NEW_PRODUCT);
}
/**
* 事件源頭以及事件型別
*
* @param p
* @param type
*/
public ProductEvent(Product p, ProductEventType type) {
this.source = p;
this.type = type;
// 事件觸發
this.notifyEventDispatch();
}
/**
* 獲得事件的始作俑者
*
* @return
*/
public Product getSource() {
return source;
}
/**
* 獲得事件的型別
*
* @return
*/
public ProductEventType getEventType() {
return this.type;
}
/**
* 通知事件處理中心
*/
private void notifyEventDispatch() {
super.addObserver(EventDispatch.getEventDispatch());
super.setChanged();
super.notifyObservers(source);
}
}
我們在產品事件類中增加了一個私有方法notifyEventDispatch,該方法的作用是明確事件的觀察者,並同時在初始化時通知觀察者,他在有參構造中被呼叫。我們再來看事件的觀察者,如下所示。
public class EventDispatch implements Observer {
// 單例模式
private final static EventDispatch dispatch = new EventDispatch();
// 事件消費者
private Vector<EventCustomer> customer = new Vector<EventCustomer>();
/**
* 不允許生成新的例項
*/
private EventDispatch() {
}
/**
* 獲得單例物件
*
* @return
*/
public static EventDispatch getEventDispatch() {
return dispatch;
}
@Override
public void update(Observable o, Object arg) {
}
}
產品和事件都定義出來了,那我們想想怎麼把這兩者關聯起來,產品和事件是兩個獨立的物件,兩者都可以獨立的擴充套件,用什麼來適應他們的擴充套件呢?橋樑模式!兩個不相關的類可以通過橋樑模式組合出穩定、健壯的結構。
我們這次把抽象化角色和實現角色去掉,各位可能要說了,把抽象化角色和實現化角色去掉,那橋樑模式在抽象層次耦合的優點還怎麼體現呢?因為我們採用的是單個產品物件,沒有必要進行抽象化處理,若要按照該框架做擴充套件開發,該部分是肯定需要抽象出介面或抽象類的,好在也非常簡單,只要抽取一下就可以了。這樣考慮後,我們的ProductManager類就增加一個功能:組合產品類和事件類,產生有意義的產品事件,如下所示。
public class ProductManager {
// 是否可以建立一個產品
private boolean isPermittedCreate = false;
/**
* 建立一個產品
*
* @param name
* @return
*/
public Product createProduct(String name) {
// 首先修改許可權,允許建立
this.isPermittedCreate = true;
Product p = new Product(this, name);
// 產生一個建立事件
new ProductEvent(p, ProductEventType.NEW_PRODUCT);
return p;
}
/**
* 廢棄一個產品
*
* @param p
*/
public void abandonProduct(Product p) {
// 銷燬一個產品,例如刪除資料庫記錄
// 產生刪除事件
new ProductEvent(p, ProductEventType.DEL_PRODUCT);
p = null;
}
/**
* 修改一個產品
*
* @param p
* @param name
*/
public void editProduct(Product p, String name) {
// 修改後的產品
p.setName(name);
// 產生修改事件
new ProductEvent(p, ProductEventType.EDIT_PRODUCT);
}
/**
* 獲得是否可以建立一個產品
*
* @return
*/
public boolean isCreateProduct() {
return this.isPermittedCreate;
}
/**
* 克隆一個產品
*
* @param p
* @return
*/
public Product clone(Product p) {
// 產生克隆事件
new ProductEvent(p, ProductEventType.CLONE_PRODUCT);
return p.clone();
}
}
在每個方法中增加了事件的產生機制,在createProduct方法中增加了建立產品事件,在editProduct方法中增加了修改產品事件,在delProduct方法中增加了遺棄產品事件,在clone方法中增加克隆產品事件,而且每個事件都是通過組合產生的,產品和事件的擴充套件性非常優秀。
剛剛我們說完了產品和事件的關係處理,現在回到我們事件的觀察者,他承擔著非常重要的職責。我們知道他要處理事件,但是現在還沒有想好怎麼實現他處理事件的update方法,暫時保持為空。
我們繼續分析,這麼多事件(現在只有1個產品類,如果產品類很多呢?)不可能每個產品事件都寫一個處理者吧,對於產品事件來說,他最希望的結果就是我通知了事件處理者(也就是觀察者模式的觀察者),其他具體怎麼處理由觀察者來解決,那現在問題是觀察者怎麼來處理這麼多的事件呢?事件的處理者必然有N多個,如何才能通知相應的處理者來處理事件呢?一個事件也可能通知多個處理者來處理,並且一個處理者處理完畢還可能通知其他的處理者,還不可能讓每個處理者獨自完成這樣“不可能完成的任務”。
是的,需要中介者模式上場了,我們把EventDispatch類作為事件分發的中介者,事件的處理者都是具體的同事類,他們有著相似的行為,都是處理產品事件,但是又有不相同的邏輯,每個同事類對事件都有不同的處理行為。
EventDispatch類有3個職責。
- 事件的觀察者
作為觀察者模式中的觀察者角色,接收被觀察者期望完成的任務,在我們的框架中就是接收ProductEvent事件。
- 事件分發者
作為中介者模式的中介者角色,他擔當著非常重要的任務——分發事件,並同時協調各個同事類(也就是事件的處理者)處理事件。
- 事件處理者的管理員角色
不是每一個事件的處理者都可以接收事件並進行處理,是需要獲得分發者許可後才可以,也就是說只有事件分發者允許他處理,他才能處理。
事件分發者擔當了這麼多的職責,那是不是與單一職責原則相違背了?確實如此,我們在整個系統的設計中確實需要這樣一個角色擔任這麼多的功能,如果強制細分也可以完成,但是會加大程式碼量,同時導致系統的結構複雜,可以考慮拆分這3個職責,然後再組合相關的功能,看看程式碼量是如何翻倍的。
注意,設計原則只是一個理論,而不是一個帶有刻度的標尺,因此在系統設計中不應該把他視為不可逾越的屏障,而是應該把他看成是一個方向標,儘量遵守,而不是必須恪守。
盡然事件分發者這麼重要,我們就仔細研讀一下他的程式碼,如下所示。
public class EventDispatch implements Observer {
// 單例模式
private final static EventDispatch dispatch = new EventDispatch();
// 事件消費者
private Vector<EventCustomer> customer = new Vector<EventCustomer>();
/**
* 不允許生成新的例項
*/
private EventDispatch() {
}
/**
* 獲得單例物件
*
* @return
*/
public static EventDispatch getEventDispatch() {
return dispatch;
}
@Override
public void update(Observable o, Object arg) {
// 事件
ProductEvent event = (ProductEvent) o;
// 處理者處理,這裡是中介者模式的核心,可以是很複雜的業務邏輯
for (EventCustomer e : this.customer) {
// 處理能力是否匹配
for (EventCustomType t : e.getCustomType()) {
if (t.getValue() == event.getEventType().getVlaue()) {
e.exec(event);
}
}
}
}
/**
* 註冊事件處理者
*
* @param _customer
*/
public void registerCustomer(EventCustomer _customer) {
this.customer.add(_customer);
}
}
我們在這裡使用Vector來儲存所有的事件處理者,在update方法中使用了兩個簡單地for迴圈來完成業務邏輯的判斷,只要事件的處理者級別和事件的型別相匹配,就呼叫事件處理者的exec方法來處理事件,該邏輯是整個事件觸發架構的關鍵點,但不是難點。請注意,在設計這樣的框架前,一定要定義好消費者與生產者之間的搭配問題,一般的做法是通過xml檔案類或者IoC容器配置規則,然後在框架啟動時載入並駐留記憶體。
EventCustomer抽象類負責定義事件處理者必須具有的行為,首先是每一個事件的處理者都必須定義自己能夠處理的級別,也就是通過建構函式來定義自己的處理能力,當然處理能力可以是多值的,也就是說一個處理者可以處理多個事件;然後各個事件的處理者只要實現exec方法就可以了,完成自己對事件的消費處理即可。我們先來看抽象的事件處理者,如下所示。
public abstract class EventCustomer {
// 容納每個消費者能夠處理的級別
private Vector<EventCustomType> customType = new Vector<EventCustomType>();
/**
* 每個消費者都要宣告自己處理哪一類別的事件
*
* @param customType
*/
public EventCustomer(EventCustomType customType) {
addCustomType(customType);
}
/**
* 每個消費者可以消費多個事件
*
* @param type
*/
public void addCustomType(EventCustomType type) {
this.customType.add(type);
}
/**
* 得到自己的能力
*
* @return
*/
public Vector<EventCustomType> getCustomType() {
return customType;
}
/**
* 每個事件都要對事件進行宣告式消費
*
* @param event
*/
public abstract void exec(ProductEvent event);
}
很簡單,我們定義了一個Vector變數來儲存處理者的處理能力,然後通過建構函式約束子類必須定義一個自己的處理能力。在程式碼中,我們用到了事件處理型別列舉,如下所示。
public enum EventCustomType {
// 新建立事件
NEW(1),
// 刪除事件
DEL(2),
// 修改事件
EDIT(3),
// 克隆事件
CLONE(4);
private int value = 0;
private EventCustomType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
我們在系統中定義了3個事件處理者,分別是乞丐、平民和貴族。乞丐只能獲得別人遺棄的物品,平民消費自己生產的東西,自給自足,而貴族則可以獲得精修的產品或者是綠色產品(也就是我們這裡的克隆產品,不用自己勞動獲得的產品)。我們先看乞丐的原始碼,如下所示。
public class Beggar extends EventCustomer {
/**
* 只能處理被人遺棄的東西
*/
public Beggar() {
super(EventCustomType.DEL);
}
@Override
public void exec(ProductEvent event) {
// 事件的源頭
Product p = event.getSource();
// 事件型別
ProductEventType type = event.getEventType();
System.out.println("乞丐處理事件:" + p.getName() + "銷燬,事件型別=" + type);
}
}
乞丐在無參構造中定義了自己只能處理刪除的事件,然後在exec方法中定義了事件的處理邏輯,每個處理者都是隻要完成這兩個方法即可,我們再來看平民級別的事件處理者,如下所示。
public class Commoner extends EventCustomer {
/**
* 定義平民能夠處理的事件的級別
*/
public Commoner() {
super(EventCustomType.NEW);
}
@Override
public void exec(ProductEvent event) {
// 事件的源頭
Product p = event.getSource();
// 事件型別
ProductEventType type = event.getEventType();
System.out.println("平民處理事件:" + p.getName() + "誕生記,事件型別=" + type);
}
}
平民只處理新建立的事件,其他事件不做處理,我們再來看貴族級別的事件處理者,如下所示。
public class Nobleman extends EventCustomer {
/**
* 定義貴族能夠處理的事件的級別
*/
public Nobleman() {
super(EventCustomType.EDIT);
super.addCustomType(EventCustomType.CLONE);
}
@Override
public void exec(ProductEvent event) {
// 事件的源頭
Product p = event.getSource();
// 事件型別
ProductEventType type = event.getEventType();
if (type.getVlaue() == EventCustomType.CLONE.getValue()) {
System.out.println("貴族處理事件:" + p.getName() + "克隆,事件型別=" + type);
} else {
System.out.println("貴族處理事件:" + p.getName() + "修改,事件型別=" + type);
}
}
}
貴族稍有不同,他有兩個處理能力,能夠處理修改事件和克隆事件,同時在exec方法中對這兩類事件分別進行處理。此時,可能會想到另外一個處理模式:責任鏈模式。建立一個鏈,然後兩類事件分別在鏈上進行處理並反饋結果。可以參考一下Servlet的過濾器的設計,在框架平臺的開發中可以採用該模式,他具有非常好的擴充套件性和穩定性。
所有的角色都已出場,我們建立一個場景類把他們串聯起來,如下所示。
public class Client {
public static void main(String[] args) {
// 獲得事件分發中心
EventDispatch dispatch = EventDispatch.getEventDispatch();
// 接受乞丐對事件的處理
dispatch.registerCustomer(new Beggar());
// 接受平民對事件的處理
dispatch.registerCustomer(new Commoner());
// 接受貴族對事件的處理
dispatch.registerCustomer(new Nobleman());
// 建立一個原子彈生產工廠
ProductManager factory = new ProductManager();
// 製造一個產品
System.out.println("=====模擬建立產品事件=====");
System.out.println("建立一個叫做小男孩的原子彈");
Product p = factory.createProduct("小男孩原子彈");
// 修改一個產品
System.out.println("\n=====模擬修改產品事件=====");
System.out.println("把小男孩原子彈修改為胖子號原子彈");
factory.editProduct(p, "胖子號原子彈");
// 再克隆一個原子彈
System.out.println("\n=====模擬克隆產品事件=====");
System.out.println("克隆胖子號原子彈");
factory.clone(p);
// 遺棄一個產品
System.out.println("\n=====模擬銷燬產品事件=====");
System.out.println("遺棄胖子號原子彈");
factory.abandonProduct(p);
}
}
我們的事件處理框架已經生效了,有行為,就產生事件,並有處理事件的處理者,並且這三者都相互解耦,可以獨立的擴充套件下去。比如,想增加處理者,沒有問題,建立一個類繼承EventCustomer,然後註冊到EventDispatch上,就可以進行處理事件了;想擴充套件產品,沒問題?需要稍稍修改一下,首先抽取出產品和事件的抽象類,然後再進行擴充套件即可。
小結
該事件觸發框架結構清晰,擴充套件性好,可以進行抽象化處理後應用於實際開發中。我們回頭看看在這個案例中使用了哪些設計模式。
- 工廠方法模式
負責產生產品物件,方便產品的修改和擴充套件,並且實現了產品和工廠的緊耦合,避免產品隨意被建立而無觸發事件的情況發生。
- 橋樑模式
在產品和事件兩個物件的關係中我們使用了橋樑模式,如此設計後,兩者都可以自由的擴充套件(前提是需要抽取抽象化)而不會破壞原有的封裝。
- 觀察者模式
觀察者模式解決了事件如何通知處理者的問題,而且觀察者模式還有一個優點是可以有多個觀察者,也就是我們的架構是可以多層級、多分類的處理者。想重新擴充套件一個新型別(新介面)的觀察者?沒有問題,擴充套件ProductEvent即可。
- 中介者模式
事件有了,處理者也有了,這些都會發生變化,並且處理者之間也有耦合關係,中介者則可以完美的處理這些複雜的關係。
我們再來思考一下,如果我們要擴充套件這個框架,可能還會用到什麼模式?首先是責任鏈模式,他可以幫助我們解決一個處理者處理多個事件的問題;其次是模板方法模式,處理者的啟用、停用等,都可以通過模板方法模式來好似西安;再次是裝飾模式,事件的包裝、處理者功能的強化都會用到裝飾模式。當然了,我們還可能用到其他的模式,只要能夠很好的解決我們的困境,那就好好使用吧,這也是我們學習設計模式的目的。