[toc]
從 javascript 的角度去了解 設計模式 與各模式的適用場景。
0. 個人對於設計模式的認識:
- 設計模式其實就是程式碼實現的各種最佳實踐,然後由前人總結歸納並取了個相應的的名字,有些時候我們在做專案的時候可能不知不覺就運用了設計模式中的一些思想。
- 前端的各種框架或工具裡也運用到了各種設計模式,例如:antd 的
Modal
,jquery中的$
, lodash 中的_
是單例模式;webApi 中的addEventListener
, jquery 中的on
方法其實都可以成為觀察者模式;很多類庫中對於瀏覽器相容性的處理算是外觀模式, 事件的冒泡與捕獲是責任鏈模式。 - 一個專案中根據需要可能會在設計的時候運用多種設計模式。
- redux 的 store 就是一個單例,其內部的 subscribe 對於dispatch的監聽,其實就是觀察者模式。compose 其實就是組合模式,將多個函式組合成一個函式, 等等......
- React 的核心其實是結合了 狀態模式與觀察者模式,觀察 state 的變化並實時更新 UI 層。
- 其實從前端的角度去理解後端的設計模式有其侷限性。畢竟 js 在目前來說大部分情況下的運用都不是物件導向的。而且大部分講設計模式的書籍都是以物件導向的方式為範例去講解的(例如java),如果從 js 層面去理解設計模式的話就不能直接用物件導向中的設計模式的概念往 js 上套,需要從另一層面去理解其內在的思想、想要解決的痛點。
- 很多建立型模式與結構型模式中,很多對於他們範例與定義是拿
類
來做說明的,但其實 js 中並沒有 類這個概念,即使是 ES6 中的class
也只是建構函式的語法糖,typeof class === 'function'
。其特性與物件導向的語言也有很大的不同。例如享元模式如果從類的建立與例項快取層面去理解就不太容易,但是從前端的 DOM 建立與複用層面去理解的話就容易得多。
- 很多建立型模式與結構型模式中,很多對於他們範例與定義是拿
- 有些設計模式的 設計理念 與 想要解決的痛點是一致的。例如單例模式與享元模式,都是為了減少物件的建立,對既有的物件進行復用,減少記憶體佔用。不過一個是在建立層面去優化,一個從應用層去優化。
- 對於前端來說,我認為比較常用到的設計模式有:單例模式、介面卡模式、裝飾器模式、外觀模式、過濾器模式、組合模式、與大部分的行為型模式。
- 單例模式可以解決單一功能物件重複使用的問題。一般用於在某個作用域的頂層。建立一次整個作用域公用這個例項。ES6 的 Symbol 型別其實就是一個單例,標識獨一無二的。
- 介面卡模式 可以解決不同模組的統一使用問題,是一層包裝。例如我們在 Angular 程式碼中引入 React 的專案作為子模組。
- 裝飾器模式 可以解決 class 的增強困難的問題,通常每個裝飾器的功能是單一的,有些時候一個類為了功能的複用可能繼承了好幾層類,這時候把單一的功能抽出來用裝飾器去增強的話程式碼更靈活解耦、容易理解、易於重構。
- 外觀模式其實本質上與介面卡模式有點相似,都是為了解決程式碼的適配問題,只不過維度不一樣。介面卡是以模組為維度,外觀是以某個功能能為維度。
- 過濾器模式場景更多,最簡單的 Array.filter 就是個過濾器。
- 組合模式其實就是各種功能、元件、甚至函式的組合,使程式碼在使用的時候更加簡潔方便。例如 redux 中
applyMiddleware
對每個中介軟體的組合,使其在發起 dispatch 的時候不需要一個個去呼叫中介軟體函式,呼叫組合好的函式就可以。 - 觀察者模式 是為了解決模組間的通訊問題,使不同的模組在相同的時間可以同步通知狀態並作出相應的反應。
1. 單例模式(Singleton)
單例模式是建立型例項,意圖產生一個類的唯一例項。
- type: 建立型模式
- 優點:
- 在記憶體裡只有一個例項,較少了記憶體的開銷,加快物件訪問速度。
- 避免對資源的多重佔用。
- 適用場景:
- 多個地方被使用,尤其是頻繁被建立、銷燬的物件(例如Dialog 或者 Toaster)。
- 適合用來記錄全域性的狀態(例如 Redux)。
- 建立物件時耗時過多或者耗資源過多,但又經常用到的物件。
- 示例:
把 Dialog 做成一個單例的模式, 全域性只有一個 Dialog 例項。
export default createDialog = ( _ => {
// 宣告 Dialog 例項的引用,其始終存在於這個閉包內
let instanceDialog;
return () => {
const content = (<div>{option.title}</div>);
class Dialog {
constructor() {
super({});
}
show(option) {
const dialogDom = document.createElement('div');
const content = (<div>{ option.title }</div>);
document.body.appendChild(dialogDom);
ReactDOM.render(content, dialogDom);
}
}
// 如果已經例項過 Dialog, 那麼就是用這個 or 例項化 Dialog
return instanceDialog || (instanceDialog = new Dialog());
};
})();
複製程式碼
使用時:
var Dialog = createDialog();
Dialog.show({title: '簡單的 Dialog'});
複製程式碼
2. 簡單工廠模式 (Simple Factory Pattern)
簡單工廠模式是建立型模式,由一個方法來決定到底要建立哪個類的例項,其所例項化的型別在建立時並不確定, 而是在使用時候根據引數決定例項化某個類。
- type: 建立型模式
- 適用場景:
- 需要根據不同引數產生不同例項,這些例項有一些共性的場景。
- 使用者只需要使用產品,不需要知道產品的建立細節。
- 示例:
建立一個學生類,每一個學生都可以在其自身的任務列表裡新增學習任務,例如家庭作業 與 問卷
// createTask 是一個函式,功能很單一,其會根據傳入的任務型別,new 一個相應的任務。這是一個工廠
const createTask = (() => {
const taskMap = {
homework: () => {
return new Homework(); // Homework 的類
},
question: () => {
return new Question(); // Question 的類
},
};
return (taskType) => {
taskMap[taskType]();
}
})();
// 這是一個Student 類,用於建立學生,其內部有一個任務列表(taskList),通過addTask方法給任務列表新增新的任務
class Student {
constructor() {
this.taskList = [];
}
addTask(taskType) {
// 呼叫簡單工廠函式去建立任務
this.taskList.push(createTask(taskType));
}
}
// 建立一個學生:小明,並給小明佈置家庭作業
const xiaoming = new Student();
xiaoming.addTask('homework');
複製程式碼
整個過程中,其實由三部分組成,
- Student: 用於建立學生,建立時不關心其是什麼樣子,分配有什麼任務。
- createTask: 這是一個工廠,根據傳入的引數建立不同的物件並返回給物件使用類(Student),內部有不同物件(task)的建立過程,如果新增taskType,需要在這裡新增。
- Homework、Question等物件類:不同的taskType產生的不同類,也就是上面的工廠(createTask)生產的產品。
3. 工廠方法模式 (Factory Method Pattern)
工廠方法模式屬於類建立型模式。工廠父類負責定義建立產品物件的公共介面,而工廠子類則負責生成具體的產品物件,這樣做的目的是將產品類的例項化操作延遲到工廠子類中完成,即通過工廠子類來確定究竟應該例項化哪一個具體產品類。
- type: 建立型模式
- 跟簡單工廠模式的不同:
- 簡單工廠模式中,所有的建立邏輯和判斷邏輯的程式碼都內聚在一個工廠中進行,根據傳入的引數例項相應的產品。這樣隨著專案越來越大,每次增加子類或者刪除子類物件的建立都需要開啟這簡單工廠類來進行修改,這個工廠也會越來越臃腫,內部有更多的
switch case
結構,程式碼耦合都會越來越高。 - 工廠方法模式,將產品的建立方法拆開來,每一個產品都有一個自己的工廠類,將產品的例項化下放到在子類中去進行,核心類就變成了抽象類。
- 簡單工廠模式中,所有的建立邏輯和判斷邏輯的程式碼都內聚在一個工廠中進行,根據傳入的引數例項相應的產品。這樣隨著專案越來越大,每次增加子類或者刪除子類物件的建立都需要開啟這簡單工廠類來進行修改,這個工廠也會越來越臃腫,內部有更多的
- 適用場景:
- 客戶端不需要知道具體產品類的類名,只需要知道所對應的工廠即可,具體的產品物件由具體工廠類建立;客戶端需要知道建立具體產品的工廠類。
- 在工廠方法模式中,對於抽象工廠類只需要提供一個建立產品的介面,而由其子類來確定具體要建立的物件
- 例項:
假如說,我們要抽象出一個 teacher 類,通過這個抽象類我們可以建立出各種各樣的老師。
// 英語老師類
class EnglishTeacher {}
// 美術老師類
class ArtTeacher {}
// 老師工廠類,其內部有各種老師的建立方法
class TeacherFactory {
englishTeacherCreator() {
this.name = '英語老師';
return new EnglishTeacher();
}
artTeacherCreator() {
this.name = '美術老師';
return new ArtTeacher();
}
}
// 建立一個英語老師
const englishTeacherFactory = new TeacherFactory().englishTeacherCreator;
const englishTeacher = englishTeacherFactory();
複製程式碼
上面的程式碼中每一種老師都有一個獨立的建立方法,例項時必須先將建立方法得到,雖然建立的步驟變多了但是更利於日後 Teacher 工廠類的擴充套件,需要新增老師型別的時候,要新增建立方法和產品類
4. 抽象工廠模式(Abstract Factory)
5. 建造者模式(Builder Pattern)
建立型模式的一種,主要關注於將各個小物件組裝成一個複雜物件的過程
- type: 結構型模式
- 優點:
- 建造者相對獨立,易擴充套件
- 示例:
肯德基裡有薯條、漢堡、可樂、雪碧、等各種各樣的單品,這些單品組成了各種花樣的套餐
class Burger {} // 漢堡類 class HotDog {} // 熱狗類 class Cola {} // 可樂類 class Sprite {} // 雪碧類 // 套餐類 class KFCPackage { conctructor() { this.stapleFood = ''; this.Drink = ''; } } // 建造者類,裡面有漢堡、熱狗、可樂等生產機 class KFCBuilder { burgerBuilder() { this.stapleFood = new Burger(); } hotDogBuilder() { this.stapleFood = new HotDog(); } colaBuilder() { this.drink = new Cola(); } spriteBuilder() { this.drink = new Sprite(); } getFood() { var foodPackage = new KFCPackage(); foodPackage.stapleFood = this.stapleFood; foodPackage.drink = this.drink; return foodPackage; } } // 漢堡套餐指揮者類 class BurgerPackageDirector { create(builder) { this.stapleFood = builder.burgerBuilder(); this.drink = builder.colaBuilder(); } } // 熱狗套餐指揮者類 class hotDogPackageDirector { create(builder) { this.stapleFood = builder.hotDogBuilder(); this.drink = builder.spriteBuilder(); } } // 建造者例項 const foodBuilder = new KFCBuilder(); // 漢堡指揮者例項 const burgerDirector = new BurgerPackageDirector(); // 熱狗指揮者例項 const hotDogDirector = new hotDogPackageDirector(); // 生產漢堡套餐 burgerDirector.create(foodBuilder); // 獲取漢堡套餐,裡面有漢堡和可樂 var burgerPackage = foodBuilder.getFood(); // 生產熱狗套餐 hotDogDirector.create(foodBuilder); // 獲取熱狗套餐,裡面有熱狗和雪碧 var hotDogPackage = foodBuilder.getFood(); 複製程式碼
- 建造者模式結合裝飾器模式
上面的KFC的例子中,每當推出一個新的套餐的時候,都要增加一個全新的指揮類,這裡的邏輯可以使用修飾器來簡化
// 套餐類與建造者不變 ...... // 指揮者裝飾器,接受食物名與飲料名兩個引數 const directorDecorator = (option) => { return (wrappedClass) => { wrappedClass.create = (builder) => { this.stapleFood = builder[option.foodName](); this.drink = builder[option.drinkName](); } return wrappedClass; } } // 指揮者基類 class basePackageDirector { static create } // 推出雞肉卷牛奶套餐 @directorDecorator({foodName: 'chickenRoll', drinkName: 'milk'}) class chickenRollDirector extends basePackageDirector; // 雞肉卷牛奶指揮者例項 const chickenRollDirector = new chickenRollDirector(); // 生產雞肉卷牛奶套餐 chickenRollDirector.create(foodBuilder); // 獲取雞肉卷牛奶套餐,裡面有雞肉卷和牛奶 var burgerPackage = foodBuilder.getFood(); 複製程式碼
- 建造者模式結合裝飾器模式
6. 介面卡模式(Adapter Pattern)
介面卡模式如字面意義上的功能,適配兩個不同的模組,使其可以一起工作,可以類比生活當中的讀卡器,使得電腦與記憶體卡之間經過讀卡器的轉換可以正常連線
-
type: 結構型模式
-
優點:
- 增加各模組的複用程度,使不相容的模組一起執行。
- 使各模組間的關係更加靈活。
-
適用場景:
- 一般不是專案一開始就設計成醬紫,而是對現有專案進行相容性改造而採用。
- 新老介面更替時對老資料的相容。
-
示例:
假如我們有兩個專案,一個是 angular1.5 的專案,一個是 React 的專案,由於效能、工程效率、複用性等原因,我們打算在將一些可以通用的元件使用React來寫,然後通過介面卡在 Angular 中複用。
// React 模組: 需要匯出介面卡需要的的成員與元件
export const platform = {
React,
ReactDOM,
Component1,
Component2,
};
// Angular 模組: 需要一個專門用於呼叫 React 元件的 directive,其接受兩個屬性,react 元件的元件名、props
var platform = require('xxx'); // 引入公用的 React 模組
var React = platform.React;
var ReactDOM = platform.ReactDOM;
Module.directive('reactConnector',
[function () {
return {
restrict: 'EA',
template: '<div></div>',
replace: true,
scope: {
componentName: "<",
props: '='
},
link: function (scope, element) {
/**
* 因為angular 中不支援 jsx,所以使用`React.createElement`建立節點,
* 將需要使用的 React 元件作為他的children,並將 props 傳進去
* 被建立出來的元件 ReactConnectorChild 內部會將他的 children render 出來
* 將創造出的 Element 掛載到 angular 節點上
*/
function render() {
ReactDOM.render(
React.createElement(ReactConnectorChild, angular.extend({
children: [
React.createElement(platform[componentName]),
]
}, scope.props)),
element[0]
);
}
// 監聽登出事件,移除節點
scope.$on('$destroy', () => {
ReactDOM.unmountComponentAtNode(element[0]);
});
render();
}
};
}]
);
// 使用 reactConnector 指令的時候
<react-connector componentName="Component1" props="props" />
複製程式碼
假如說我想要在angular模組內實時監聽 React 模組中內部state的變化,這時候可以使用 觀察者模式 建立一個單例的模組
// 建立一個模組,專門用來進行 React 模組的 state 發生改變的時候派發事件
export const stateListener = (() => {
let dispatchEvent, listenerEvent;
const eventMap = {}; // 儲存事件的容器
// return 一個函式,用以移除對state的監聽
const removeListener = (key) => {
return () => {
delete eventMap[key];
}
};
listenerEvent = (key, callback) => {
// 將監聽函式加入事件模型
eventMap[key] = callback;
return removeListener(key);
};
dispatchEvent = (key, state) => {
const eventCallback = eventMap[key];
eventCallback && eventCallback(state);
};
return {
listenerEvent,
dispatchEvent,
};
}())
/** Angular 模組
* reactConnector 指令中,可以新加入兩個引數
* listenedName,與 listenedCallback,用於規定監聽某個 React 元件的 state
* 由於React元件是通用的,所以請保持 listenedName 是單一的
* 登出時清掉 監聽
*/
import { stateListener } from 'xxx';
link: function (scope, element) {
// ...
const listener = stateListener.listenerEvent(scope.listenedName, scope.listenedCallback);
scope.$on('$destroy', () => {
// ...
listener();
});
}
/** React 模組,可以建立一個基類
* 其在 DidUpdate 的時候就派發事件並將更新過的state傳遞出去,事件名為定好的 listenedName
* 需要同步state的話,通用元件就可以去繼承它
*/
import { stateListener } from 'xxx';
export default class basePlatform extends React.Component {
componentDidUpdate(prevProps, prevState) {
const listenedName = this.props.ListenedName;
if (listenedName) {
stateListener.dispatchEvent(listenedName, prevState);
}
}
}
複製程式碼
7. 裝飾器模式(Decorator pattern)
這種模式是建立一個修飾器類,用來包裝原有的類,並在保持被包裝的類方法簽名完整性的前提下,提供額外的功能,其實所謂的高階元件就是一個裝飾器。
-
type: 結構型模式
-
使用場景:
- 需要對原有的類進行擴充套件,但又不想使用繼承的方式增加一些子類。
- 需要大範圍的對既有的類進行功能上的擴充套件。
- 其實 react-redux 中的 connect 方法,就是返回了一個修飾器函式,用於包裝傳入的。 WrappedComponent
-
優點:
- 通用功能可以簡單便捷地進行的複用。
- 裝飾類和被裝飾類可以獨立發展,不會相互耦合。
-
與介面卡模式的區別:
- 其跟介面卡模式有相似之處,都是通過封裝其他物件達到設計的目的,但是它們的形態有很大區別。
- 介面卡是為了包裝某個模組的介面,從而使兩個不同模組之間能實現連線,比如上面舉的將 React 模組放在 Angular 的指令中的例子。
- 而裝飾器是為了僅僅是包裝現有的模組,給其增加功能,不改變其內部結構與原有功能。
-
示例:
比如說,為了保障我們現在專案上資源的安全性,需要在全網所有有關資料與列表的頁面加水印,如果我們一個頁面一個頁面的去加相同的功能的話,會非常的費時費力,而且還容易出錯。這時候就可以寫一個裝飾器,在需要加水印的元件上包裝一下。
/** * 裝飾器函式,其接受兩個引數 * WrappedComponent為需要進行包裝的元件, option 為需要的引數,如水印的大小,透明度等 * return 一個包裝過的元件,其有了新增水印的功能 */ const watermarkDecorator = (WrappedComponent, option) => { return class Connect extends React.Component { constructor(props: IProps) { super(props); this.waterType = option.waterType; } render() { const props = this.props; // 在傳入的元件外包一層節點,並新增水印,不改變原元件原有的功能 const backStyle = { backgroundImage: 'url(xxx.png)', }; return( <div className="wrapped-watermark" style={backStyle}> <WrappedComponent {...props} /> </div> ); } }; }; // 使用的時候,將容器元件用這個裝飾器包裝一下就可以 const someComponent = watermarkDecorator(WrappedComponent, option); 複製程式碼
如果專案中支援 ES7 語法的話還可以使用修飾器語法來進行包裝元件,不過修飾器的寫法要變一下,因為ES7修飾器只支援一個引數
const watermarkDecorator = (option) => { return (WrappedComponent) => { return class Connect extends React.Component { constructor(props: IProps) { super(props); this.waterType = option.waterType; } render() { const props = this.props; // 在傳入的元件外包一層節點,並新增水印,不改變原元件原有的功能 const backStyle = { backgroundImage: 'url(xxx.png)', }; return( <div className="wrapped-watermark" style={backStyle}> <WrappedComponent {...props} /> </div> ); } }; }; }; // 使用的時候,直接在原來的元件上加上修飾器並傳入引數就可以 @watermarkDecorator(option) class someComponent { // ... } 複製程式碼
- 裝飾器模式結合簡單工廠模式
回想上面簡單工廠模式裡面的例子,假如說我們建立學生的時候,可能會小學生、高中生等不同型別的學生怎麼辦?可能需要每一個學生型別都要去寫一個class,或者可以寫一個基類,然後讓不同型別的學生去繼承,但是高中生又分化為理科生與文科生呢?那文科生又要去繼承高中生的類。隨著學生型別越分越細,繼承下來依賴的類越來越多,相似功能的方法也會越來越多,程式碼的正交性也會變差,後期的可維護也會降低。所以我們可以把Student作為一個基類,如果需要擴充套件不同的型別的學生,直接用修飾器去修飾。
以 Student 為基類,使用修飾器建立出普通學生與美術特長生兩種類,不同的學生有不同預設的學習任務
這樣,再需要有新的學生型別,只需要去建立新的修飾器新增特定的行為,各個修飾器可以進行組合// 新增學習任務的工廠函式 const createTask = (() => { const taskMap = { homework: () => { return new Homework(); }, English4: () => { return new English4(); }, art: () => { return new Art(); }, }; return (taskType) => { taskMap[taskType](); }; })(); // Student 基類,建立子類的時候只需要給其新增修飾器 class Student { static addTask(taskType) { // 呼叫簡單工廠函式去建立任務 this.taskList.push(createTask(taskType)); } taskList = []; } // 高中生的修飾器 const heightStudentDecorator = (WrappedClass) => { WrappedClass.addTask('English4'); return WrappedClass; }; // 美術生的修飾器 const artStudentDecorator = (WrappedClass) => { WrappedClass.addTask('art'); return WrappedClass; }; @heightStudentDecorator class HeightStudent extends Student; @artStudentDecorator @heightStudentDecorator class ArtStudent extends Student; // 建立一個普通學生: 小明 const xiaoming = new Student(); // 建立一個高中生: 小剛 const xiaogang = new HeightStudent(); // 建立一個高中美術特長生: 小紅 const xiaohong = new ArtStudent(); 複製程式碼
- 裝飾器模式結合簡單工廠模式
8. 代理模式 (Proxy Pattern)
代理模式的定義是把對一個物件的訪問, 交給另一個代理物件來操作。
-
type: 結構型模式
-
適用場景:
- 建立一個物件需要很大的成本,可以通過一個代理物件來代表一個大物件
- 為了安全性考慮不允許直接訪問物件,通過一個代理來作為其的訪問層。
-
優點:
- 模組職責清晰:主類只關心其核心業務。
- 代理物件可以在訪問者和目標物件之間起到中介的作用,保護目標物件。
-
示例:
考試的時候,需要根據分數的高低來決定去哪個學校,這個分數線是由教育局定製的,但是我們又不能直接去教育局問可以去哪個學校,而是通過一個教育局的代理查詢系統來檢視。
// 教育局的類 class EducationBureau { static goToHarvard(name) { console.log(name + '去哈佛大學'); return new Harvard(); } static goToTsinghua(name) { console.log(name + '去清華大學'); return new Tsinghua(); } static goToHomedun(name) { console.log(name + '去家裡蹲大學'); return new Homedun(); } } // 代理查詢系統的類 class Counselor { static distributionSchool(student) { var studentName = student.name; var studentFraction = student.fraction; if (studentFraction > 80) { EducationBureau.goToHarvard(studentName); } else if (studentFraction > 60) { EducationBureau.goToTsinghua(studentName); } else { EducationBureau.goToHomedun(studentName); } } } // 學生類,可以傳入其學生資訊,包括考試分數 class Student { constructor(studentInfo) { this.name = studentInfo.name; this.fraction = studentInfo.fraction; } } // 當需要查詢一個學生的可報名院校時,將學生資訊輸入系統就可以 const xiaoming = new Student({name: 'xiaoming', fraction: 99}); Counselor.distributionSchool(xiaoming); // xiaoming 去哈佛大學 const xiaohong = new Student({name: 'xiaohong', fraction: 0}); Counselor.distributionSchool(xiaohong); // xiaohong 去家裡蹲大學 複製程式碼
-
代理模式 配合 介面卡模式
繼續上面的例子,雖然現在學生可以輸入成績查詢錄取院校了,但是真實情況是全國有不同的試卷,比如說全國卷、山東卷、江蘇卷。每個學校對不同卷的分數線也是不同的,比如說清華大學對北京地區學生的錄取分數線是86分,對山東地區學生的錄取分數線是90分。這時候不同戶籍的學生就要去不同地區的查詢系統查詢。
// 江蘇卷修飾器 const JiangsuTestPaperDecorator = (WrappedClass) { const scoreLineMap = { Harvard: 90, Tsinghua: 85, Homdun: 60, }; WrappedClass.scoreLineMap = scoreLineMap; } // 山東卷修飾器 const ShandongTestPaperDecorator = (WrappedClass) { const scoreLineMap = { Harvard: 99, Tsinghua: 83, Homdun: 60, }; WrappedClass.scoreLineMap = scoreLineMap; } // 代理查詢系統的基類 class Counselor { static scoreLineMap = { Harvard: 88, Tsinghua: 80, Homdun: 70, } static distributionSchool(student) { const studentName = student.name; const studentFraction = student.fraction; const scoreLineMap = Counselor.scoreLineMap; if (studentFraction > scoreLineMap.Harvard) { EducationBureau.goToHarvard(studentName); } else if (studentFraction > scoreLineMap.Tsinghua) { EducationBureau.goToTsinghua(studentName); } else { EducationBureau.goToHomedun(studentName); } } } // 江蘇卷的類 @JiangsuTestPaperDecorator Class JiangsuCounselor extends Counselor; // 山東卷的類 @ShandongTestPaperDecorator Class ShandongCounselor extends Counselor; // 查詢入口,這也是一個代理,接受學生資訊,然後再根據學生戶籍分配不同的系統去查詢 const distributionArea = (studentInfo) => { switch (studentInfo.census) { case 'jiangsu': JiangsuCounselor.distributionSchool(studentInfo); // 江蘇卷查詢系統 break; case 'shandong': ShandongCounselor.distributionSchool(studentInfo); break; default: Counselor.distributionSchool(studentInfo); // 全國卷查詢系統 } } // 來自江蘇的小明 const xiaoming = new Student({ name: 'xiaoming', fraction: 91, // 分數 census: jiangsu // 戶籍 }); // 來自山東的小紅 const xiaohong = new Student({ name: 'xiaohong', fraction: 91, census: shandong }); distributionArea(xiaoming); // xiaoming 去哈佛大學 distributionArea(xiaohong); // xiaohong 去清華大學 複製程式碼
這個例子用了兩個代理,一個是 distributionArea,用於根據不同地區的學生分配對應的查詢系統,一個是各個查詢系統(Counselor),用於根據其不同的分數線呼叫教育局的錄取證照。
用到了給 Counselor 新增 scoreLineMap 的修飾器,將全國卷的查詢系統修飾為地區級別的查詢系統9. 享元模式 (Flyweight Pattern)
結構型模式的一種,主要用於減少建立物件的數量,以減少記憶體佔用和提高效能。通過減少物件數量從而改善應用所需的物件結構的方式。
- type: 結構型模式
- 適用場景:
- 有大量的相似結構的物件
- 這些物件的部分狀態可以放在外面
- 優點:
- 減少了物件的建立,減少記憶體佔用,提高效率。
- 示例:
作為前端我首先想到的大量的、重複物件的場景是 UI 列表,比如說,我要建立一個學生列表,這個列表裡有三種學生,文科生、理科生、藝術生。
建立一個 Student 類,每一種學生都可以共享其內部狀態 -> 學生型別的對映 typeMap, 在render時,由外部傳入學生名字,生成不同的學生例項。// 學生享元類,可能被用於重複建立學生物件 class Student extends React.Component { constructor(props) { this.typeMap = { culture: '文科生', science: '理科生', art: '藝術生' }; this.type = this.props.type; } render(name) { this.studentType = typeMap[this.type] return ( <li>{name} 是 {studentType}</li> ); } } // 列表容器,學生將在這裡展示,接受一個學生列表 class StudentList extends React.Component { constructor(props) { this.state = { studentInstance: [], // 存放學生例項 }; } conponentDidMount() { this.createStudentFactory(); } // 這是一個建立學生的 工廠 createStudentFactory() { const studentList = this.props.studentList; const studentInstance = []; // 將學生按照文理科進行劃分的map // 如果已經建立過該學生型別的話,就複用裡面的。否則新建立物件 const studentMap = {}; studentList.forEach((item) => { const studentType = studentMap[item.type]; if (studentType) { studentInstance.push(studentType.render(item.name)); } else { studentType = new Student(item); studentInstance.push(studentType.render(item.name)); } }) this.setState({studentInstance}); } render() { return ( <ul> {this.state.studentInstance} </ul> ) } } // 三個學生的列表 const studentList = [ {name: xiaogang, type: science}, {name: xiaowu, type: science}, {name: xiaohong, type: art} ]; ReactDOM.render( <StudentList studentList={studentList} />, document.querySelector('body') ); 複製程式碼
上面的列表中,雖然展示了三個學生,[小剛,小吳,小紅],但是其實只建立了兩個學生物件,因為小剛跟小吳都是理科生, 在建立學生的時候複用了該型別的物件。
-
享元模式 配合 代理模式
開學的時候公佈錄取結果,有個複雜的物件為學校,其內部有名字,型別等資訊,還可以建立學生,因為他足夠複雜,且大多雷同。所以想把它做成一個享元類以儘可能的複用,同時,在建立學校的工廠中新增代理用來新增學生。
// 學校享元類 class School { constructor(props) { this.name = props.schoolName; this.studentList = []; } // 建立學生,加入本校學生列表並log歡迎語 createStudent(studentInfo) { this.studentList.push(studentInfo); console.log(`歡迎${studentInfo.name}`同學加入${this.name}); } } // 建立學校的工廠,主要功能為建立學校,如果建立過就記錄下來,沒有則建立 // 而且,其中代理了學校的 createStudent 方法,將學生加入學校 const addStudentToSchool = (() => { const schoolMap = {}; return (studentInfo) => { const schoolName = studentInfo.admissionSchool; const joinSchool = schoolMap[schoolName]; if (joinSchool) { // 代理學校的 createStudent 方法將學生加入學校 joinSchool.createStudent(studentInfo); } else { schoolMap[schoolName] = newSchool({ schoolName }); schoolMap[schoolName].createStudent(studentInfo); } } }()) const xiaokai = { name: '小凱', admissionSchool: '哈佛大學', }; addStudentToSchool(xiaokai); // -> 歡迎小凱同學加入哈佛大學 const xiaohong = { name: '小紅', admissionSchool: '濟南大學', }; addStudentToSchool(xiaohong); // -> 歡迎小紅同學加入濟南大學 複製程式碼
上面的例子中,School 是一個享元類,其共享相同大學的資訊,無論有多少個不同的學生,只要他們的錄取學校是相同的,就只會生成一個 school 物件。
10. 橋接模式 (Bridge Pattern)
結構型模式的一種,目的是將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
-
type: 結構型模式
-
優點:
- 將抽象部分與例項部分相分離。
- 有時候,為了實現一個功能,但是這個功能上有多個類的影子,我們可能會一層層繼承各個類以獲得他們的特性。複用性比較差,而且多繼承結構中類的數量可能會比較多,橋接模式是比多繼承方案更好的解決方法。
- 提高了抽象與例項的可擴充性,兩個維度可以獨立擴充套件,都不需要修改已有的功能。
-
示例:
假如說我們建立學生的時候,可能會有幼兒園、初中、大學等不同型別的學生,他們的行為可能不一樣,例如幼兒園早上吃奶粉,中學生中午做操、大學生早上吃煎餅中午上自習。
這個例子跟在修飾器模式中舉的例子相似,修飾器的例子中是將各種型別學生的行為做成了修飾器,使用的時候去一層層組裝,這裡用橋接模式實現這個場景。// 早上 action 類 class MorningAction { constructor(morningModel) { this.foodName = morningModel.foodName; } action() { const actionDesc = `早上吃${this.foodName}`; console.log(actionDesc); } } // 中午 action 類 class NoonAction { constructor(noonModel) { this.actionName = noonModel.actionName; } action() { const actionDesc = `中午${this.actionName}`; console.log(actionDesc); } } // 學生類 class Student { constructor(morningModel, noonModel) { morningModel && this.morningAction = new MorningAction(morningModel); noonModel && this.noonAction = new NoonAction(noonModel); } actionTrack() { this.morningAction && this.morningAction.action(); this.noonAction && this.noonAction.action(); } } // 學前班學生 -> 早上吃奶粉 var preschoolStudent = new Student({foodName: '奶粉'}); // 中學生 -> 中午做操 var middleSchoolStudent = new Student(null, {actionName: '做操'}); // 大學生 -> 早上吃煎餅,中午上自習 var collegeStudents = new Student({foodName: '煎餅'}, {actionName: '上自習'}); 複製程式碼
上面的例子建立了一個 Student 類,在例項的時候根據傳入引數決定生成的學生的行為,不傳對應的屬性則沒有該行為。後期隨著學生型別的越來越多,如果要擴充套件 Student 類,新增引數與行為類就可以,不影響之前的例項物件。擴充套件例項學生時也不會影響 Student 類。
- 橋接模式 結合 享元模式
此時,可以根據傳入引數位置與value的不同來建立學生了,但是 Student 每次建立學生都必須去new 一個 Student 物件,我想讓相同型別的學生共享一些資訊,例如學生型別、行為;
// 早上 action 類 class MorningAction { constructor(type) { this.type = type; } action(name, morningModel) { const actionDesc = `${name}早上${morningModel.courseName}`; console.log(actionDesc); } } // 下午 action 類 class AfternoonAction { constructor(type) { this.type = type; } action(name, afternoonModel) { const actionDesc = `${name}下午${afternoonModel.sportName}`; console.log(actionDesc); } } // 學生享元類 class Student { constructor(type, morningModel, afternoonModel) { this.type = type; this.morningModel = morningModel; this.afternoonModel = afternoonModel; this.morningAction = morningModel && new MorningAction(type); this.afternoonAction = afternoonModel && new AfternoonAction(type); } actionTrack(name) { this.morningAction && this.morningAction.action(name, this.morningModel); this.afternoonAction && this.afternoonAction.action(name, this.afternoonModel); } } // 建立學生的工廠,建立並享元 Student 物件 const studentFactory = (() => { const studentTypeMap = {}; return (studentInfo) => { var studentInstance = studentTypeMap[studentInfo.type]; if (studentInstance) { studentInstance.actionTrack(studentInfo.name); } else { studentTypeMap[studentInfo.type] = new Student({...studentInfo}); studentTypeMap[studentInfo.type].actionTrack(studentInfo.name); } } }()) // 小紅早上升國旗 studentFactory({ type: 'primaryStudent', name: '小紅', morningModel: { courseName: '升國旗' }, }); // 強東早上概率論,強東下午踢足球 studentFactory({ type: 'collegeStudents', name: '強東', morningModel: { courseName: '概率論' }, afternoonModel: { sportName: '踢足球' } }); 複製程式碼
- 橋接模式 結合 享元模式
11. 外觀模式(Facade Pattern)
結構型模式的一種,外部與一個子系統的通訊必須通過一個統一的外觀物件進行,為子系統中的一組介面提供一個一致的介面,外觀模式定義了一個高層介面,這個介面使得這一子系統更加容易使用。
-
type: 結構型模式
-
優點:
- 減少系統相互依賴。
- 提高靈活性。
- 在內部可以做一些安全措施。
-
跟代理模式的區別:
- 代理模式是在一個類中去呼叫另一個物件的方法,當使用代理模式的時候,我們常常在一個代理類中建立一個物件的例項。
- 外觀模式是通過外觀的包裝,使應用程式只能看到外觀物件,而不會看到具體的細節物件。外觀模式注重的是多個類的整合、統一適配。
-
示例:
例如我們要寫一個方法,這個方法的目的是解決各個版本的瀏覽器對addEventListener的相容問題。
const addEventListener = (element, e, fn) => { if (element.addEventListener) { element.addEventListener(e, fn, false); } else if( element.attachEvent('on' + e, fn); ) else { element['on' + e] = fn; } } 複製程式碼
-
外觀模式 結合 工廠模式
建立一個 person 類,其有一個獲取衣服的方法,在獲取衣服時候會自動根據 person 的特徵建立 clothes 例項
// 裙子類
class Skirt {
constructor(property) {
console.log('符合女士property的裙子');
}
}
class Jeans {
constructor(property) {
console.log('符合男士property的牛仔褲');
}
}
const createClothes = ((personInfo) => {
let property;
if (type === 'woman') {
property = {
height: personInfo.height,
hair: personInfo.hair,
bust: personInfo.bust,
};
return new Skirt(property)
} else {
property = {
height: personInfo.height,
};
return new Jeans(property)
}
})();
class Person {
constructor(personInfo) {
this.name = personInfo.name;
this.type = personInfo.type;
}
getClothes() {
const clothes = createClothes(this.type);
this.clothes = clothes;
}
}
// 建立一個185身高的男生,並給他搭配合適的牛仔褲
const xiaogang = new Person({name: 'xiaogang', height: 185});
xiaogang.getClothes();
// 建立一個150身高、長髮、胸小的女生, 並給她搭配合適的牛仔褲
const xiaohong = new Person({name: 'xiaohong', height: 150, hair: '1m', bust: 'A'});
xiaohong.getClothes();
複製程式碼
12. 過濾器模式(Filter Pattern)
這是一種結構型模式,允許開發者使用不同的標準來過濾一組物件,通過邏輯運算以解耦的方式把它們連線起來。
- type: 結構型模式
- 適用場景:
- 例如請求的反向代理,根據不同的請求代理到不同的伺服器
- 對陣列的過濾,例如Array.filter
示例: 現在有一組 student 物件,現在老師想以各種維度來過濾符合相應條件的學生
// 有一組資料,裡面有名字 與 成績資訊
const studentList = [
{name: '張三', score: 80},
{name: '劉能', score: 40},
{name: '劉麗紅', score: 99}
];
// 過濾出姓 ‘劉’ 的過濾器
const lastnameFilter = (list) => {
return list.filter((v) => {
return /劉/.test(v.name);
});
}
// 過濾出分數 > 60 的過濾器
const scoreFilter = (list) => {
return list.filter((v) => {
return v.score > 60;
});
}
// 這是一個過濾器組合器,可以將任意多個過濾器組合成一個
const filterCompose = (...arg) => {
const funcs = [...arg];
// 如果只有一個,返回此過濾器
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(function (a, b) {
return function () {
return a(b.apply(undefined, arguments));
};
});
}
// 獲取姓 ‘劉’ 的學生
const liuStudents = lastnameFilter(studentList);
// 獲取姓劉 && 分數 > 60 的學生
// 生成組合的過得過濾器
const combinFilter = filterCompose(lastnameFilter, scoreFilter);
combinFilter(studentList);
複製程式碼
- 過濾器模式 配合 外觀模式
上面的例子,使用了 Array.filter 用來過濾一個列表,現在想用一個方法,接受array 與 obiect 兩種型別,返回一個 list ,過濾出所有符合刪選條件的物件的屬性或陣列的分量
上面的例子,filter 是一個過濾器,其內部根據傳入引數的不同型別分別呼叫不同的方法進行過濾,外部使用時不需要關心其內部邏輯/** * 過濾出符合條件的物件屬性 或 陣列分量 * @param 需要過濾的物件或陣列 * @param 過濾條件 * @returns [array] */ filter = (obj, cb) => { if (obj instanceof Array) { return obj.filter(cb); } else if (typeof obj === 'object') { const list = []; for (let key in obj) { const objValue = obj[key]; if (cb(objValue)) { list.push[objValue]; } } return list; } else { return obj; } } // 過濾 string 的過濾器 const stringFilter = (item) => { return typeof item === 'string'; } var arr = ['1', 2, '3']; var obj = {a: '1', b: 2, c: '3'}; filter(arr, stringFilter); // ['1', '3'] filter(obj, stringFilter); // ['1', '3'] 複製程式碼
13. 組合模式(Composite Pattern)
這是一種結構型模式,是用於把一組相似的物件當作一個單一的物件。組合模式依據樹形結構來組合物件,用來表示部分以及整體層次。
- type: 結構型模式
- 優點:
- 將物件組合成樹形結構以表示"部分-整體"的層次結構。
- 組合模式使得使用者對單個物件和組合物件的使用具有一致性。
- 客戶程式可以向處理簡單元素一樣來處理複雜元素。
- 示例:
假如說我們有一個表單,其表單內有各個控制元件,當表單提交的時候,我們需要將所有控制元件都獲取到,一個一個校驗,這無疑是個重複的體力活。此時,我想用一個容器元件將這些表單控制元件組合起來,需要校驗的時候只需要呼叫容器元件的 validate 就可以校驗所有的控制元件。
// textarea 控制元件
class Textarea {
render() {
return (
<textarea ref="inputRef" />
);
}
validate() {
const value = this.refs.inputRef.value;
return value.length > 5;
}
}
// checkbox 控制元件
class Checkbox {
render() {
return (
<checkbox ref="inputRef" />
);
}
validate() {
const value = this.refs.inputRef.value;
return value === 1;
}
}
// 容器元件
class composeForm extend React.Component {
render() {
return (
<div>
<Checkbox ref="CheckboxRef" />
<Textarea ref="TextareaRef" />
</div>
);
}
validate() {
const refs = this.refs;
for (key in refs) {
refs[key].validate();
}
}
}
複製程式碼
所有的子控制元件可以包在 composeForm 中,包括 composeForm 元件自己。每一個表單控制元件都有一個 validate 方法用於校驗,所有的子控制元件被組合成了一個樹狀結構的表單,需要校驗的時候,只需要呼叫頂層的 composeForm 元件的 validate 方法,即可以調動所有子控制元件的 validate 方法進行校驗。實現了使用者對單個物件和組合物件的使用具有一致性
,客戶程式可以向處理簡單元素一樣來處理複雜元素
。
這個例子還可以升級改造,現在 composeForm 內的子元件是死的,composeForm 的作用也並不純粹,可以使 composeForm 更加純粹僅作為一個容器存在,把內部所有的 dome 結構抽象出來,使用時作為配置傳進去。
- 組合模式 結合 工廠方法模式
上面的例子中,composeForm 元件是作為多個表單控制元件的組合容器,但現在他是不純粹的,裡面是固定的表單內容。我想把它抽象一下,由外部傳入表單的配置檔案,然後 composeForm 內部根據配置檔案動態的去建立
控制元件例項
並進行組合。
// 表單控制元件的工廠類,裡面有各種控制元件的建立方法
class InputengFactory {
text() {
return new TextInput();
}
textArea() {
return new Textarea();
}
select() {
return new Select();
}
// ......
}
/**
* 表單控制元件的容器,在其內部建立相應的控制元件例項
*/
class FormItem extend React.Component {
constructor(props) {
super(props);
}
render() {
return this.instanceInput();
}
/**
* 例項化表單控制元件的方法
* 建立控制元件工廠並例項化控制元件
*/
instanceInput() {
const option = this.props.option;
const inputengFactory = new InputengFactory();
const instanceInput = InputengFactory[option.type](option);
this.element = instanceInput;
return instanceInput;
}
// 校驗控制元件
validate() {
this.element && this.element.validate();
}
}
/**
* compose 元件,接受 model 引數,是一個配置列表,並進行組合
* @props {Array} [{type: 'text', label: '標籤', value: 'string', validate: fnc}]
*/
class composeForm extend React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{this.renderFormList()}
</div>
);
}
renderFormList() {
const model = this.props.model || [];
return model.map((item) => {
<FormItem option={item} ref={item.name}/>
});
}
validate() {
const refs = this.refs;
for (key in refs) {
refs[key].validate();
}
}
}
複製程式碼
這裡例子中更有四種角色,
- composeForm 組合元件,作用是接受配置檔案並進行單元組合
- FormItem 表單控制元件的容器元件,是 composeForm 組合中的單元,在其內部進行建立控制元件
- InputengFactory 控制元件工廠類,裡面有各種控制元件的建立方法
- TextInput、Select 等控制元件產品類
14. 觀察者模式 (Observer Pattern)
觀察者模式(釋出者-訂閱者模式),其實就是字面意義上的釋出與訂閱的一種關係模式,訂閱一個約定好的時間,釋出的時候通知訂閱者。
-
type: 結構型模式
-
適用場景:
- 可用於兩個模組間的通訊
- 其實對於javascript這種事件驅動的語言來說說,觀察者模式使用非常廣泛,例如通過 addEventListener 去監聽某些事件,就是在訂閱事件的觸發時機。
- 再例如所熟悉的 Angular 中的事件傳遞系統,
$emit、$on
, 再例如Redux的 subscribe 也是觀察者模式。
-
示例:
簡單實現 Angular 的事件傳遞, 生成一個
$scope
例項,其有兩個屬性$emit
與$on
,在註冊監聽$on
的時候,會返回一個函式,用來移除監聽。
const Scope = () => {
let $emit, on;
const eventMap = {}; // 儲存事件的容器
// return 一個函式,用以移除相應的監聽
const $del = (key) => {
return () => {
delete eventMap[key];
}
};
// 訂閱者
$on = (key, callback) => {
// 如果已經註冊過對此時間的監聽,排除一個錯誤
if (eventMap[key]) {
throw (`已註冊對 ${key} 的監聽`);
} else {
// 將監聽函式加入事件模型
eventMap[key] = callback;
return $del(key);
}
};
// 釋出者,釋出時將引數傳入相應的監聽函式並執行。
$emit = (key, option) => {
const eventCallback = eventMap[key];
eventCallback && eventCallback(option);
};
return {
$emit,
$on,
};
}
// 例項化一個 $scope
var $scope = Scope();
// 訂閱 getUp 事件
var delGetUp = $scope.$on('getUp', (say) => {
console.log('Hi boy,' + say);
});
// 釋出 getUp 事件, 得到控制檯log:Hi boy, Good morning
$scope.$emit('getUp', 'Good morning');
// 移除掉對 getUp 的監聽
delGetUp();
/** 也可以把 $scope 做成一個單例的,免得建立不同的 $scope 事件訂閱機制被擾亂 **/
// 使用的時候用 createScope() 建立 $scope,只會被建立一次,再建立會返回之前的 $scope
const createScope = ( _ => {
let $scope;
return () => {
return $scope || ($scope = scope());
};
})();
複製程式碼
15. 責任鏈模式 (Chain of Responsibility Pattern)
責任鏈模式為請求建立了一個接收者物件的鏈。每個接收者都包含對另一個接收者的引用。如果一個物件不能處理該請求,那麼它會把相同的請求傳給下一個接收者,依此類推。
- type: 行為型模式
- 優點:
- 避免請求傳送者與接收者耦合在一起,讓多個物件都有可能接收請求,將這些物件連線成一條鏈,並且沿著這條鏈傳遞請求,直到有物件處理它為止。
- 適用場景:
- 事件的冒泡與捕獲
- 針對不同角色的同一行為的分層處理
- 例項:
有這麼一個場景,比如說,在瀏覽器上點選了一個按鈕,我想要獲取到這個按鈕上級元素的有我想要的類名或者其他屬性的上級元素。
/**
* 獲取某個元素的上級元素中,符合搜尋條件的,離他最近的,元素的某個屬性值
* @params ele 起點元素
* @params filter 過濾型別。如class、tagName等
* @params value 過濾型別的過濾條件
* @returns element
*/
const getDomContent = (ele, filter, value) => {
let resElement = resElement;
if(filter === 'class') {
resElement = getClassElement(ele, value);
} else if (filter === 'tagName') {
resElement = getTagElement(ele, value)
}
return resElement;
}
// 不斷向上尋找符合 class 條件的DOM
const getClassElement(ele, value) {
let parentElement = ele.parentElement;
if (parentElement.classList.indexOf(value) !== -1) {
return parentElement;
} else if(parentElement.tagName !== null) {
classElementFilter(ele, value);
}
throw('未找到符合該類名的上級元素');
}
// 不斷向上尋找符合 tagName 條件的DOM
const getTagElement = (ele, value) => {
let parentElement = ele.parentElement;
if (parentElement.tagName !== value) {
return parentElement;
} else if (parentElement.tagName !== null) {
classElementFilter(ele, value);
}
throw('未找到符合該標籤的上級元素');
}
// 使用時, 獲取 button 所離他最近的類名為container的父元素
const ele = document.querySelector('button');
const whantDom = getDomContent(ele, 'class', 'container');
複製程式碼
- 責任鏈模式 配合 過濾器模式
有這麼一個場景,我想要在頁面上給所有類名為 ‘item’ 的 div 元素新增
16. 迭代器模式(Iterator Pattern)
這種模式用於順序訪問集合物件的元素,不需要知道集合物件的底層表示。
- type: 行為型模式
- 示例:
建立一個迭代器類, 並利用來迭代一個物件,只可以被迭代一次
/**
* 迭代器類,接受一個物件並生成一個迭代器
* hasNext 屬性,用來判定還有沒有下一個可迭代屬性
* next 獲取下一個屬性
*/
class Iterator {
constructor(iteratorObj) {
this.iteratorObj = iteratorObj;
this.index = 0;
this.iteratorKeys = Object.keys(iteratorObj);
}
hasNext() {
return this.index < this.iteratorKeys.length;
}
next() {
if(this.hasNext()){
const nextKey = this.iteratorKeys[this.index++];
return this.iteratorObj[nextKey];
}
return null;
}
}
const obj = {a: 1, b: 2, c: 3};
// 生成一個迭代器
const objIterator = new Iterator(obj);
// 迭代
while(objIterator.hasNext()) {
console.log(objIterator.next()); // => 1, 2, 3, 4
}
複製程式碼
- ES6 中的Iterator
ES6 中可迭代物件是包含
Symbol.iterator
屬性的物件,這個Symbol.iterator
屬性對應著能夠返回該物件的迭代器的函式。在ES6中,所有的集合物件(陣列、Set和Map)以及字串都是可迭代物件,因此它們都被指定了預設的迭代器。可迭代物件都可以與ES6中新增的for-of迴圈配合使用。