天天看點

設計模式混編:觀察者模式+中介者模式

觀察者模式

▐  應用背景

在軟體系統設計中,我們經常會有這樣的需求:如果一個對象的狀态發生改變,某些與它相關的對象也要随之做出相應的變化。比如我們會有以下幾個場景訴求:

  • 如果一個使用者關注了一個公衆号,那便會收到公衆号發來的消息。
  • 使用者的一個點選事件,會觸發界面背景顔色的變更、收到一個背景消息等。

由于關聯事件的廣泛應用,是以觀察者模式也是在項目中經常使用的模式之一。

▐  基本介紹

觀察者模式包含觀察目标和觀察者兩類對象,一個目标可以有任意數目的與之相依賴的觀察者,一旦觀察目标的狀态發生改變,所有的觀察者都将得到通知。

基于上述應用場景的表達,我們可以來看一下該模式的定義。觀察者模式(Observer Pattern) 也叫做釋出訂閱模式(Publish/Subscribe),定義對象間一種一對多的依賴關系,使得每當一個對象改變狀态時,則所有依賴于它的對象都會得到通知并被自動更新(Define a one-to-many dependency between objects so that when one object changes state,all its dependents are notified and updated automatically)。

觀察者模式作為一種對象行為型模式,解決的問題依然是兩個關聯對象間(觀察者和被觀察者)的耦合。

其通用的類圖如下所示:

設計模式混編:觀察者模式+中介者模式

在觀察者模式中,會存在幾個角色:

  • Subject 被觀察者

    定義被觀察者必須實作的職責, 它必須能夠動态地增加、 取消觀察者。它一般是抽象類或者是實作類, 僅僅完成作為被觀察者必須實作的職責:管理觀察者并通知觀察者。

  • Observer觀察者

    觀察者接收到消息後,通過update操作進行處理。

  • ConcreteSubject具體被觀察者

    在基于被觀察者自己通用的業務邏輯上,同時可以定義對哪些事件進行通知。

  • ConcreteObserver具體觀察者

    消息接收後,有着不同的處理邏輯。

觀察模式核心代碼:

//被觀察者public abstract class Subject {  //定義一個觀察者數組  private Vector<Observer> obsVector = new Vector<Observer>();  //增加一個觀察者  public void addObserver(Observer o){  this.obsVector.add(o);  }    //删除一個觀察者  public void delObserver(Observer o){  this.obsVector.remove(o);  }    //通知所有觀察者  public void notifyObservers(){        for(Observer o:this.obsVector){            o.update();        }  }}
//觀察者public interface Observer {  //更新方法  public void update();}      

▐  優缺點分析

優點

  • 觀察者和被觀察者之間抽象耦合

    在擴充能力上,不管是增加觀察者還是被觀察者都是比較簡單的。

  • 建立一套觸發機制

    在設計理念上,我們任務每個類的職責都是單一的,即單一職責原則。那麼,通過觀察者模式我們可以将多個類串聯起來,形成一個觸發鍊,建構一個觸發機制。

缺點

在應用觀察者模式時,我們需要對開發效率和運作效率特别注意。比如,通用的消息通知形式是以順序執行的,如果一個觀察者出現異常,将會導緻整體的執行異常,同時處理時間也嚴格與每一個觀察者節點有關,是累加的關系。是以在複雜場景下,需要考慮采用異步的方式。

▐  使用場景

事件多級觸發場景、跨系統間的消息交換場景等。

▐  注意事項

在使用觀察者模式時,有一個廣播鍊的問題需要特别注意到。

  • 廣播鍊問題

    在設計觀察者模式時,會存在這樣的一種考慮,一個對象具有雙重身份,即又是觀察者,也是被觀察者。這樣,消息就可以在多個節點間傳播(和責任鍊模式有一點點類似,都是建立傳播鍊)。但是該鍊式結構一旦建立,邏輯就比較複雜,可維護性也是非常差的。根據經驗建議,一個觀察者模式中,最多出現一個對象既是觀察者也是被觀察者,即消息最多轉發一次,建立三個節點的傳播鍊,這樣是比較好控制的。

中介模式

DataX是集團内被廣泛使用的離線資料同步工具/平台,實作包括 MySQL、Oracle、HDFS、Hive、OceanBase、HBase、OTS、ODPS 等各種異構資料源之間高效的資料同步功能。

DataX其實相當于一個中介,從資料源讀取資料,同步寫入到目标端,資料源不再需要維護到目标端的同步作業,隻需要與 DataX 通信即可。DataX展現了中介者模式的思想。

中介者模式(Mediator Pattern):用一個中介對象封裝一系列的對象互動,中介者使各對象不需要顯示地互相作用,進而使其耦合松散, 而且可以獨立地改變它們之間的互動(Define an object that encapsulates how a set of objectsinteract.Mediator promotes loose coupling by keeping objects from referring to each other explicitly,and it lets you vary their interaction independently)。

顯然,中介模式解決的問題依舊是耦合關系。

其通用的類圖如下:

設計模式混編:觀察者模式+中介者模式
設計模式混編:觀察者模式+中介者模式

中介模式下存在三個角色,主要包括:

  • Mediator 抽象中介者

    抽象中介者角色定義統一的接口,用于各同僚角色之間的通信。

  • Concrete Mediator 具體中介者

    通過協調各同僚角色實作協作行為,是以它必須依賴于各個同僚角色。

  • Colleague 同僚角色

    每個同僚角色都會維持者一個對抽象中介者類的引用,可以在需要和其他同僚對象通信時,先與中介者通信,通過中介者來間接完成與其他同僚類的通信。此外,同僚類的行為包括兩種,一種是改變自身狀态、處理自己行為的自發行為;另一種則是依賴中介者才能完成的依賴行為。

中介者模式的核心是引入了中介者類,那麼中介者類主要承擔了兩個責任,其一是中轉作用,通過中介者提供的中轉作用,各個同僚對象就不再需要顯式引用其他同僚;其二是協調作用,中介者可以更進一步的對同僚之間的關系進行封裝和分離,而同僚類僅需保持一緻的和中介者進行互動即可。

中介模式核心代碼:

//抽象中介者類public abstract class Mediator {    //定義同僚類    protected ConcreteColleague1 c1;    protected ConcreteColleague2 c2;    //通過getter/setter方法把同僚類注入進來    public ConcreteColleague1 getC1() {        return c1;    }    public void setC1(ConcreteColleague1 c1) {        this.c1 = c1;    }    public ConcreteColleague2 getC2() {        return c2;    }    public void setC2(ConcreteColleague2 c2) {        this.c2 = c2;    }    //中介者模式的業務邏輯    public abstract void doSomething1();    public abstract void doSomething2();}//中介者類public class ConcreteMediator extends Mediator {    @Override    public void doSomething1() {        //調用同僚類的方法, 隻要是public方法都可以調用        super.c1.selfMethod1();        super.c2.selfMethod2();    }    public void doSomething2() {        super.c1.selfMethod1();        super.c2.selfMethod2();    }}      

中介者模式減少了類間的互相依賴,它用中介者和同僚的一對多互動代替了原來同僚之間的多對多互動,一對多關系更容易了解、維護和擴充,将原本難以了解的網狀結構轉換成相對簡單的星型結構。

中介者模式内中介者會膨脹得很大, 而且包含着大量同僚類之間的互動細節,原本N個對象直接的互相依賴關系轉換為中介者和同僚類的依賴關系,同僚類越多,中介者的邏輯就越複雜。

中介者模式适用于多個對象之間緊密耦合的情況,緊密耦合的标準是在類圖中出現了蜘蛛網狀結構。中介者模式可以将蜘蛛網狀結構轉變為星型結構,使原本複雜混亂的關系變得清晰且簡單。

事件觸發器最佳實踐

▐  事件觸發器任務

今天,我們的任務是來模拟一個事件觸發器。有一個産品,它有多個觸發事件,它産生的時候會觸發一個建立事件,在修改的時候觸發修改事件,删除的時候觸發删除事件,初始化的時候要觸發一個onCreat事件,修改的時候觸發onChange事件,輕按兩下的時候又觸發onDoubleClick事件,例如Button按鈕。

根據基本任務需求來看,對于多事件動作,我們可以采用工廠方法模式。此外,考慮我們的産品(如GUI設計)經常會使用複制粘貼操作,是以我們也是有一個很明顯的設計模式可以使用-原型模式。結合到權限問題,也就是我們的産品并不是誰想産生就産生的,否則觸發建立事件的門檻也太容易和簡單了。是以,在設計中,我們的産品隻能由工廠類建立,而不能被其他對象通過new方式建立,那麼在這裡,單來源調用(Single Call)方法可以解決。

由此我們可以看一下UML圖:

設計模式混編:觀察者模式+中介者模式

實作邏輯比較簡單,在此可以注意一下isCreateProduct方法和Product的構造函數為何傳遞進來了一個工廠對象ProductManager。沒錯,正是解決産品生産的權限問題。

在工廠類ProductManager中定義了一個私有變量isPermittedCreate,該變量隻有在工廠類的createProduct函數中才能設定為true。在建立産品的時候,産品類Product的構造函數要求傳遞工廠對象,然後判斷是否能夠建立産品,即使你想使用類似這樣的方法:

Productp=new Product(newProductManager(),"abc");      

也是不能建立出産品的。是以說在産品類中限制了兩個生産條件,第一是必須是目前有效的工廠,第二就是拿到了生産資格。是以單來源調用的定義就很明顯了,我們将這種一個對象隻能由固定的對象初始化的方法叫做單來源調用。

//Product産生一個新的産品public Product(ProductManager manager, String name) {    //允許建立産品    if (manager.isCreateProduct()) {        canChanged = true;        this.name = name;    }}      

産生事件的對象有了之後,我們就觸發事件了。與此同時,也是要考慮事件的處理對象的。自然而然,觀察者模式就可以出場了。那麼,UML可以更新為如下所示:

設計模式混編:觀察者模式+中介者模式

觀察者模式的設計架構已基本顯現,被觀察者是Product産品,觀察者是EventDispatch事件分發器,具體事件處理我們後面講,消息的傳播對象是ProductEvent事件。接下來,我們看一下具體代碼細節。

/** * @author la.lda * @date 2020-12-07 */public enum ProductEventType {    //建立一個産品    NEW_PRODUCT(1),    //删除一個産品    DEL_PRODUCT(2),    //修改一個産品    EDIT_PRODUCT(3),    //克隆一個産品    CLONE_PRODUCT(4);    private int value = 0;    ProductEventType(int value) {        this.value = value;    }    public int getValue() {        return this.value;    }}      

ProductEventType定義了4個事件類型,分别是建立、修改、删除以及克隆。

在産品事件對象中,增加了一個私有方法notifyEventDispatch,該方法的作用就是增加事件觀察者,并在有參構造進行初始化時被調用,通知觀察者。

前面說到,我們采用工廠模式對多事件進行處理,如建立、删除等。而現在産品和事件作為兩個獨立的對象,如何将兩者進行組合關聯呢?那工廠類就需要新增一個功能,組合産品和事件,産生有價值的産品事件。

ProductManager的代碼如下:

/** * @author la.lda * @date 2020-12-07 */public class ProductManager {    //是否可以建立一個産品    private boolean isPermittedCreate = false;    //建立一個産品    public Product createProduct(String name) {        //首先修改權限,允許建立        isPermittedCreate = true;        Product product = new Product(this, name);        //産生一個建立事件        new ProductEvent(product, ProductEventType.NEW_PRODUCT);        return product;    }    //廢棄一個産品    public void abandonProduct(Product product) {        //銷毀一個産品,例如删除資料庫記錄        new ProductEvent(product, ProductEventType.DEL_PRODUCT);        product = null;    }    //修改一個産品    public void editProduct(Product product, String name) {        //修改後産品        new ProductEvent(product, ProductEventType.EDIT_PRODUCT);        product.setName(name);    }    //獲得是否可以建立一個産品    public boolean isCreateProduct() {        return isPermittedCreate;    }    //克隆一個産品    public Product clone(Product product) {        //産生克隆事件        new ProductEvent(product, ProductEventType.CLONE_PRODUCT);        return product.clone();    }}      

可以看出,每個事件動作下面都增加了事件産生機制,通過組合的形式,産品和事件在擴充性上都有很強的提升。

講述完被觀察者以及廣播消息後,我們來看一下下遊節點-觀察者。剛才說到,我們建構了一個事件分發器,為什麼要有如此的設計呢?可以想象的到,對于一個事件,自然會有多個處理者,而且一個處理者處理完之後還可能通知其他的處理者。在擴充能力上,我們有新處理者加入之後是否會影響到現有的設計架構呢?是以,本文另外一個設計模式-中介模式就可以上場了。我們将EventDispatch作為中介者,事件的分發器,而事件的處理這都是具體的同僚類,它們有獨立的處理産品事件的邏輯。當然,我們既然有了一個中心控制,也是可以增加一個功能-權限管理,即EventDispatch能決定觀察者能處理什麼事件,不能處理什麼事件。

是以,EventDispatch有三個職責:

  • 事件的觀察者
  • 事件分發器
  • 事件處理者管理者

那麼,我們現在可以完成最後一輪設計結構上的更新了。

設計模式混編:觀察者模式+中介者模式

事件分發器EventDispatch代碼:

import java.util.ArrayList;import java.util.Observable;import java.util.Observer;/** * @author la.lda * @date 2020-12-07 */public class EventDispatch implements Observer {    //單例模式    private final static EventDispatch dispatch = new EventDispatch();    //事件消費者    private ArrayList<EventCustomer> customerList = new ArrayList<EventCustomer>();    //不允許生成新的執行個體    private EventDispatch() {    }    //獲得單例對象    public static EventDispatch getEventDispatch() {        return dispatch;    }    @Override    public void update(Observable o, Object arg) {        //事件的源頭        Product product = (Product) arg;        //事件        ProductEvent event = (ProductEvent) o;        //處理者處理,這裡是中介者模式的核心,可以是很複雜的業務邏輯        for (EventCustomer e : customerList) {            //處理能力是否比對            for (EventCustomType t : e.getCustomType()) {                if (t.getValue() == event.getEventType().getValue()) {                    e.exec(event);                }            }        }    }    //注冊事件處理者    public void registerCustomer(EventCustomer customer) {        customerList.add(customer);    }}      

在EventDispatch裡使用ArrayList存儲所有的事件處理者,然後在update方法中使用了比較簡單for循環完成業務邏輯的判斷,其中第一層輪詢事件處理者,第二層則輪詢事件處理者所能處理的事件類型。隻要有事件處理者的處理類型和事件類型相比對,則調用事件處理方法exec,進入具體事件處理者的特定邏輯中。

在這裡我們對事件處理者也抽象了一層,抽象類EventCustomer定義了事件處理者通用的能力,也标示出事件處理者必須具備的行為,即定義每個處理者的處理類型。當然,這裡也是能夠處理多個事件的。

import java.util.ArrayList;/** * @author la.lda * @date 2020-12-07 */public abstract class EventCustomer {    //容納每個消費者能夠處理的級别    private ArrayList<EventCustomType> customType = new ArrayList<EventCustomType>();    //每個消費者都要聲明自己處理哪一類别的事件    public EventCustomer(EventCustomType type) {        addCustomType(type);    }    //每個消費者可以消費多個事件    public void addCustomType(EventCustomType type) {        customType.add(type);    }    //得到自己的處理能力    public ArrayList<EventCustomType> getCustomType() {        return customType;    }    //每個事件都要對事件進行聲明式消費    public abstract void exec(ProductEvent event);}      

接下來,定義三個具體的事件處理者,分别對不同僚件類型進行業務邏輯處理。

/** * @author la.lda * @date 2020-12-07 */public class Senior extends EventCustomer {    public Senior() {        super(EventCustomType.EDIT);        super.addCustomType(EventCustomType.CLONE);    }    @Override    public void exec(ProductEvent event) {        //事件的源頭        Product p = event.getSource();        //事件類型        ProductEventType type = event.getEventType();        if (type.getValue() == EventCustomType.CLONE.getValue()) {            System.out.println("進階專家處理事件:" + p.getName() + "克隆,事件類型=" + type);        } else {            System.out.println("進階專家處理事件:" + p.getName() + "修改,事件類型=" + type);        }    }}      
/** * @author la.lda * @date 2020-12-07 */public class Middle extends EventCustomer {    public Middle() {        super(EventCustomType.DEL);    }    @Override    public void exec(ProductEvent event) {        //事件的源頭        Product p = event.getSource();        //事件類型        ProductEventType type = event.getEventType();        System.out.println("中級專家處理事件:" + p.getName() + "銷毀,事件類型=" + type);    }}      
/** * @author la.lda * @date 2020-12-07 */public class Primary extends EventCustomer {    public Primary() {        super(EventCustomType.NEW);    }    @Override    public void exec(ProductEvent event) {        //事件的源頭        Product p = event.getSource();        //事件類型        ProductEventType type = event.getEventType();        System.out.println("初級專家處理事件:" + p.getName() + "誕生記,事件類型=" + type);    }}      

最後,我們建立自己的場景,看一下執行情況。

/** * @author la.lda * @date 2020-12-07 */public class Client {    public static void main(String[] args) {        //獲得事件分發中心        EventDispatch dispatch = EventDispatch.getEventDispatch();        dispatch.registerCustomer(new Senior());        dispatch.registerCustomer(new Middle());        dispatch.registerCustomer(new Primary());        //建立一個原子彈生産工廠        ProductManager factory = new ProductManager();        //制造一個産品        System.out.println("=====模拟建立産品事件========");        System.out.println("建立一個嫦娥衛星5号");        Product p = factory.createProduct("嫦娥衛星5号");        //修改一個産品        System.out.println("\n=====模拟修改産品事件========");        System.out.println("把嫦娥衛星5号修改為嫦娥衛星6号");        factory.editProduct(p, "嫦娥衛星6号");        //再克隆一個原子彈        System.out.println("\n=====模拟克隆産品事件========");        System.out.println("克隆嫦娥衛星6号");        factory.clone(p);        //遺棄一個産品        System.out.println("\n=====模拟銷毀産品事件========");        System.out.println("遺棄嫦娥衛星6号");        factory.abandonProduct(p);    }}      
設計模式混編:觀察者模式+中介者模式

我們的事件處理架構已經生效了,有行為,就産生事件,觸發事件處理,三者都互相解耦互相獨立,在擴充性上有很大的提升。如果想增加處理者,則建立一個繼承EventCustomer的類,然後注冊到EventDispatch,就可以進行事件處理了。

▐  觸發器擴充

回顧整個設計流程,感覺某些地方還是可以優化的。如果想擴充産品,也就是觀察者想要觀察多個産品,如何改進呢?

在結合最開始所說,産品是采用單來源調用方式由工廠生産的,那麼我們可以進行簡單修正來實作産品的擴充性。

/** * @author la.lda * @date 12/13/20 */public class ProductA extends Product {    public ProductA(ProductManager manager, String name) {        super(manager, name, "A");    }}      
/** * @author la.lda * @date 12/13/20 */public class ProductB extends Product {    public ProductB(ProductManager manager, String name) {        super(manager, name, "B");    }}      

從代碼結構上可知,抽象Product,以産品類型type來标示子類,工廠通過方法簽名生産出不同産品類型的産品。由此,我們徹底完成了觀察者、消息以及被觀察者的解耦和擴充任務,就可以歡樂的寫業務邏輯了。

至此,我們以觀察者模式和中介模式為主,采用多種經典設計模式,完成了一個可支援多事件分發、處理的事件觸發器。

設計模式混編總結

事件觸發架構的設計,結構清晰,擴充性良好,同時也運用了不同的設計模式。

  • 原型模式

    負責對象克隆和拷貝的功能。

  • 工廠方法模式

    負責生産和管理産品對象。

  • 觀察者模式解決了事件如何通知處理者的問題。
  • 中介者模式完美地處理了事件和處理者之間的複雜關系,解決多處理者之間的耦合關系,應對快速的業務變。

當然,業務的變化也是無窮的,我們可基于該架構的局部進行不斷更新和改進,融入更多的優秀設計模式,提高系統在穩定性、複用性以及擴充性方面的能力。

參考書籍《設計模式之禅-設計模式混編》