中介者設計模式——業務實踐

YJSON發表於2018-11-03

定義:中介者設計模式是通過中介物件封裝一系列物件之間的互動,使物件之間不再相互引用,降低他們之間的耦合。

中介者設計模式和觀察者設計模式一樣,都是通過訊息的收發機制實現的,在觀察者模式中,一個物件既可以是訊息的傳送者也是訊息的接收者,物件之間資訊交流依託於訊息系統實現解耦。而中介者模式中訊息傳送送方只有一個,就是中介物件,而且中介物件不能訂閱訊息,只有那些活躍物件(訂閱者)才可訂閱中介者的訊息,簡單的理解可以看作是將訊息系統封裝在中介者物件內部,所以中介者物件只能是訊息的傳送者。

實現原理

image.png | left | 728x414

建立中介者物件(排程中心)

廢話不多說直接上程式碼;

// eventeimtter.js

// 建立中介者物件(排程中心)
class EventEimtter {
  constructor() {
    // 建立訊息物件
    this.$event = {};
  }
  /**
   * 檢測訊息物件是否存在,不存在則初始化該訊息
   * @param {*} event
   */
  checkEvent(event) {
    if (!this.$event) {
      this.$event = {};
    }

    if (!this.$event[event]) {
      this.$event[event] = [];
    }
  }
  /**
   * 訂閱訊息
   * @param {*} type 訊息型別
   * @param {*} action
   * @param {*} context 訊息作用域上下文
   */
  on(type, action, context = null) {
    this.checkEvent(type);
    this.$event[type].push(action.bind(context));
    return this;
  }
  /**
   * 傳送訊息
   * @param {*} type
   * @param  {...any} args
   */
  emit(type, ...args) {
    if (!this.$event[type]) {
      this.$event[type] = [];
    }
    this.$event[type].forEach(func => {
      func(...args);
    });
    return this;
  }
  /**
   * 僅能傳送一次
   * @param {*} type
   * @param {*} action
   * @param {*} scope 作用域
   */
  once(type, action, scope = null) {
    this.checkEvent(type);
    const newfn = (...args) => {
      this.off(type, action);
      action.call(scope, ...args);
    };
    this.on(type, newfn);
    return this;
  }
  /**
   * 移除已經訂閱的訊息
   * @param {*} type
   * @param {*} action
   */
  off(type, action) {
    const $event = this.$event[type];
    if ($event) {
      for (let i in $event) {
        if ($event[i] === action) {
          $event.splice(i, 1);
          break;
        }
      }

      if (!$event.length) {
        delete this.$event[type];
      }
    }

    return this;
  }
  /**
   * 移除某個的型別訊息
   * @param {*} type
   */
  removeListener(type) {
    delete this.$event[type];
    return this;
  }
  /**
   * 移除所有訂閱訊息
   */
  removeAllListener() {
    this.$event = null;
    return this;
  }
  /**
   * 獲取所有的訊息型別
   */
  getEvent() {
    return this.$event;
  }
}


export default EventEimtter;
複製程式碼

小試牛刀,可否一用

在這裡,我只需要訂閱兩個訊息,然後讓中介者釋出;看看是否能夠釋出成功。

//單元測試
import EventEimtter from './eventeimtter';

const event = new EventEimtter();

// 訂閱 demo 訊息,執行回撥函式 ———— 輸出 first
event.on('demo', () => {
  console.log('first');
});
// 訂閱 demo 訊息,執行回撥函式 ———— 輸出 second
event.on('demo', () => {
  console.log('second');
})

// 釋出 demo 訊息
event.emit('demo')
// first
// second
複製程式碼

業務價值的產生,實際開發中的實踐

先說痛點,在實際的專案開發中一個頁面 js 可能有十幾個 class 類;你所見到的程式碼會是這樣的。

image.png | left | 827x1279

以上程式碼中,可以看出一個 React 元件,完全不見 React 周期函式,類函式過多 ,render 函式過於龐大;監聽的方法也很多,閱讀,維護,迭代成功過高。這段程式碼不管是對於開發者本身還是維護者,都不友好;迫切需要程式碼拆分,且實現結構層次清晰。

然而實際開發中,業務變更、迭代過快,有的業務本身複雜度極高,一個專案經手人也很多。如果程式碼不整潔,後來人就很難看懂,人們往往會對難以看懂的程式碼失去耐心,不願意進一步瞭解。如果不能進一步瞭解一部分程式碼,也就難以改進它,這樣的後果可能有兩點:

  • 重構,程式碼被拋棄
  • 直接複製這段程式碼在別的地方使用

下面是我站在前端的角度去思考業務:

image.png | left | 827x664

  1. 業務資料:負責獲取業務資料
  2. 業務邏輯:實現產品所定義的規則
  3. 邏輯資料:通過一系列規則所產出的邏輯資料
  4. 檢視資料:通過邏輯資料轉換成檢視資料(不將邏輯和檢視直接繫結)
  5. 檢視展示:通過檢視資料,直接驅動檢視層展示對應檢視
  6. 檢視功能:通過檢視展示組裝成的需求功能

在簡單的業務需求中,可能我拿到的後端資料,就直接可以去渲染檢視層,然後就完善功能。從開發的成本和複雜度上考量上,是不值得去做業務拆分。所以,在複雜的業務需求中以及兼顧拆分和維護中,這種業務方法論就可以大展手腳了。以下,我就拿開頭的例子,詳細解析圍繞業務的6大部分的設計。

專案實踐

我始終堅信技術的價值是在業務中產生的,技術本身是沒有價值的,技術的價值取決於是否能在專案中落地以及解決業務的痛點。作為中介者模式在專案中的落地,先舉一個小栗子!

image.png | left | 827x265

需求列表如下
  • 一個分頁表格, 分別有網點名稱、網點地址、聯絡電話、操作欄四列。
  • 每一行操作欄有三個按鈕,分別是 桌位管理、頁面裝修、功能設定

一般要求:使用 zent 分頁表格 Table 元件,配置好 columns ,操作欄定製渲染;更加簡易的擴充以及敏捷的操作,當然維護和開發的成本也需要考慮的。

使用 zent table 元件開發,受益於 React 資料驅動的思想,columns 是以 props 傳入;columns 中的定製渲染,可能需要涉及到父子元件之間的通訊。

在正常的開發中,我們可以這麼做。

const event = new EventEimtter();


const columns = [
  ...,
  {
    title: '操作',
    bodyRender: (rowData) => {
      return (
        <div>
          <Button onClick={() => {
            event.emit('page-decoration', rowData)
          }}> 桌位裝修 </Button>
          <Button onClick={() => {
            event.emit('desk-manage', rowData)
          }}> 桌位裝修 </Button>
          <Button onClick={() => {
            event.emit('action-setting', rowData)
          }}> 桌位裝修 </Button>
        </div>
      );
    }
  },
  ....
]

// Action 訊息處理函式實體類,業務邏輯原始碼
class Action {
  handlerPageDecoration() {
    ...
  }
  handlerDeskManage() {
    ...
  }

  handlerActionSetting() {
    ...
  }
}

const action = new Action()


class Demo extends Component {
  componentWillMount() {
    // 訂閱訊息
    event.on('page-decoration', action.handlerPageDecoration, this)
    event.on('desk-manage', action.handlerDeskManage, this)
    event.on('action-setting', action.handlerActionSetting, this)
  }

  render() {
    return (
      <Table columns={columns} ...props/>
    );
  }

  componentWillUnmount() {
    // 當該元件銷燬時,取消所以監聽事件;否則記憶體會炸掉
    event.removeAllListener();
  }
}
複製程式碼
生命週期的使用時機

image.png | left | 827x841

React 生命週期

  • constructor:儘量簡潔,只做最基本的 state 初始化
  • willMount: 一些內部使用變數的初始化
  • render: 觸發非常頻繁,儘量只做渲染相關的事情
  • didMount: 一些不影響初始化的操作應在這裡完成,比如根據瀏覽器不同進行操作,ajax獲取資料,監聽 document 事件等(server render)。
  • willUnmount:銷燬操作,銷燬計時器、銷燬自己的事件監聽等
  • willReceiveProps: 當有 props 做 state 時,監聽 props 的變化去改變 state,在這個生命週期裡 setState 不會觸發兩次渲染
  • shouldComponentUpdate:手動判斷元件是否應該更新,避免因為頁面更新做成的無謂更新,元件的重點優化之一。
  • willUpdate:在 state 變化後如果需要修改一些變數,可以在這裡執行
  • didUpdate: 與 didMount 類似,進行一些不影響到 render 的操作, update 相關的生命週期裡最好不要做 setState 操作,否則容易造成死迴圈。
在 React 生命週期中,實踐業務資料轉換

image.png | left | 826x667

業務資料的來源:

  • ReactCompoent 在 willMount 時,初始化的 state、props中獲取
  • didMount 時 Ajax 獲取的資料 業務邏輯(業務規則):
  • 處理業務規則的原始碼,根據不同的規則,對業務資料進行處理
  • 產生邏輯資料
  • 需要在 constructor  或者 willMount  中完成業務邏輯的訂閱 邏輯資料:
  • 使用業務邏輯處理產生,同步到檢視資料 試圖資料:
  • 同步邏輯資料的,中間可加 hook 檢視展示:
  • 根據檢視資料單項 render

深耕業務開發與設計

image.png | left | 827x410

總結

同觀察者模式一樣,中介者模式的主要業務也是通過模組間或者物件間的複雜通訊,來解決模組間或物件的耦合。對於中介者物件的本質是分裝多個物件的互動,並且這些物件的互動一般都是中介者內部實現的。

與外觀模式的封裝特性相比,中介者模式對多個物件的互動封裝,且這些物件一般處於同一層面上,並且封裝的互動在中介者內部,而外觀模式封裝的目的是為了提供更簡單的易用介面,而不會新增其他功能。

相關文章