中介者設計模式——業務實踐
定義:中介者設計模式是通過中介物件封裝一系列物件之間的互動,使物件之間不再相互引用,降低他們之間的耦合。
中介者設計模式和觀察者設計模式一樣,都是通過訊息的收發機制實現的,在觀察者模式中,一個物件既可以是訊息的傳送者也是訊息的接收者,物件之間資訊交流依託於訊息系統實現解耦。而中介者模式中訊息傳送送方只有一個,就是中介物件,而且中介物件不能訂閱訊息,只有那些活躍物件(訂閱者)才可訂閱中介者的訊息,簡單的理解可以看作是將訊息系統封裝在中介者物件內部,所以中介者物件只能是訊息的傳送者。
實現原理
建立中介者物件(排程中心)
廢話不多說直接上程式碼;
// 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
類;你所見到的程式碼會是這樣的。
以上程式碼中,可以看出一個 React 元件,完全不見 React 周期函式,類函式過多 ,render 函式過於龐大;監聽的方法也很多,閱讀,維護,迭代成功過高。這段程式碼不管是對於開發者本身還是維護者,都不友好;迫切需要程式碼拆分,且實現結構層次清晰。
然而實際開發中,業務變更、迭代過快,有的業務本身複雜度極高,一個專案經手人也很多。如果程式碼不整潔,後來人就很難看懂,人們往往會對難以看懂的程式碼失去耐心,不願意進一步瞭解。如果不能進一步瞭解一部分程式碼,也就難以改進它,這樣的後果可能有兩點:
- 重構,程式碼被拋棄
- 直接複製這段程式碼在別的地方使用
下面是我站在前端的角度去思考業務:
- 業務資料:負責獲取業務資料
- 業務邏輯:實現產品所定義的規則
- 邏輯資料:通過一系列規則所產出的邏輯資料
- 檢視資料:通過邏輯資料轉換成檢視資料(不將邏輯和檢視直接繫結)
- 檢視展示:通過檢視資料,直接驅動檢視層展示對應檢視
- 檢視功能:通過檢視展示組裝成的需求功能
在簡單的業務需求中,可能我拿到的後端資料,就直接可以去渲染檢視層,然後就完善功能。從開發的成本和複雜度上考量上,是不值得去做業務拆分。所以,在複雜的業務需求中以及兼顧拆分和維護中,這種業務方法論就可以大展手腳了。以下,我就拿開頭的例子,詳細解析圍繞業務的6大部分的設計。
專案實踐
我始終堅信技術的價值是在業務中產生的,技術本身是沒有價值的,技術的價值取決於是否能在專案中落地以及解決業務的痛點。作為中介者模式在專案中的落地,先舉一個小栗子!
需求列表如下
- 一個分頁表格, 分別有網點名稱、網點地址、聯絡電話、操作欄四列。
- 每一行操作欄有三個按鈕,分別是 桌位管理、頁面裝修、功能設定
一般要求:使用 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();
}
}
複製程式碼
生命週期的使用時機
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 生命週期中,實踐業務資料轉換
業務資料的來源:
- ReactCompoent 在 willMount 時,初始化的 state、props中獲取
- didMount 時 Ajax 獲取的資料 業務邏輯(業務規則):
- 處理業務規則的原始碼,根據不同的規則,對業務資料進行處理
- 產生邏輯資料
- 需要在
constructor
或者willMount
中完成業務邏輯的訂閱 邏輯資料: - 使用業務邏輯處理產生,同步到檢視資料 試圖資料:
- 同步邏輯資料的,中間可加 hook 檢視展示:
- 根據檢視資料單項 render
深耕業務開發與設計
總結
同觀察者模式一樣,中介者模式的主要業務也是通過模組間或者物件間的複雜通訊,來解決模組間或物件的耦合。對於中介者物件的本質是分裝多個物件的互動,並且這些物件的互動一般都是中介者內部實現的。
與外觀模式的封裝特性相比,中介者模式對多個物件的互動封裝,且這些物件一般處於同一層面上,並且封裝的互動在中介者內部,而外觀模式封裝的目的是為了提供更簡單的易用介面,而不會新增其他功能。