本文是一系列 ts 的設計模式實戰總結,並非單純的介紹設計模式,而是從工做中的例子出發,由於這樣才能讓人體會到設計模式離咱們很近。全篇每一個設計模式都是從,概念、一句話歸納、優缺點、實戰幾個方面來說。前端
設計模式(Design pattern)表明了最佳的實踐,一般被有經驗的面向對象的軟件開發人員所採用。設計模式是軟件開發人員在軟件開發過程當中面臨的通常問題的解決方案。這些解決方案是衆多軟件開發人員通過至關長的一段時間的試驗和錯誤總結出來的。vue
若是咱們把代碼編程比做是戰爭的話,那麼設計模式就是兵法。不會兵法確定打不過會兵法的,即便能打過也要付出更多的代價。react
設計模式,是一套被反覆使用、多數人知曉的、通過分類編目的、代碼設計經驗的總結。git
目的:使用設計模式是爲了可重用代碼,讓代碼更容易被他人理解、保證代碼的可靠性。github
開閉原則
對擴展開放,對修改關閉。保證程序的擴展性好,易於維護和升級單一職責原則
對一個類而言,應該僅有一個引發它變化的緣由里氏代換原則
子類能夠擴展父類的功能,可是不能改變父類原有的功能依賴倒置原則
抽象不依賴細節,細節應該依賴抽象。接口隔離原則
創建單一接口,代替龐大臃腫的接口。最小知識原則
一個對象應該對其餘對象有最少的瞭解。類間解耦,弱耦合。保證一個類僅有一個實例,並提供一個訪問它的全局訪問點算法
優勢
vue-router
減小內存開支
單例模式在內存中只有一個實例,減小內存開支,特別是一個對象須要頻繁地建立銷燬時,並且建立或銷燬時性能又沒法優化,單例模式就很是明顯了減小性能開銷
因爲單例模式只生成一個實例,因此,減小系統的性能開銷,當一個對象產生須要比較多的資源時,如讀取配置,產生其餘依賴對象時,則能夠經過在應用啓動時直接產生一個單例對象,而後永久駐留內存的方式來解決。避免對資源的多重佔用
例如一個寫文件操做,因爲只有一個實例存在內存中,避免對同一個資源文件的同時寫操做設置全局的訪問點
優化和共享資源訪問,例如,能夠設計一個單例類,負責全部數據表的映射處理。缺點
vuex
// 懶漢式,只有在調用 getInstance 的時候纔會實例化 Singleton
class Singleton {
static instance = null;
// 獲取實例方法
static getInstance() {
return this.instance || (this.instance = new Singleton());
}
}
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 == instance2); // true
複製代碼
// 餓漢式,在類初始化的時候就已經建立好了實例
class Singleton {
static instance = new Singleton();
// 獲取實例方法
static getInstance() {
return this.instance;
}
}
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 == instance2); // true
複製代碼
定義一個建立對象的接口,讓其子類本身決定實例化哪個工廠類,工廠模式使其建立過程延遲到子類進行編程
咱們明確地計劃不一樣條件下建立不一樣實例時小程序
優勢
缺點
好比我要有一個 Animal
工廠,這個工廠要生產動物。那麼我要定義動物都有 Feature
特徵必需要有 color
顏色跟 bark
叫聲。
// 定義工廠須要的動物特徵
interface Feature {
color: string;
bark(): void;
}
// 定義動物類型名字
type name = 'cat' | 'dog'
// 子類必需要實現 Feature 接口的方法
// 這樣咱們就能夠建立白色叫聲喵喵喵的貓了
class Cat implements Feature {
color = "白色";
bark() {
console.log(`${this.color} 喵喵喵`);
}
}
// 建立 Dog 類
class Dog implements Feature {
color = "黑色";
bark() {
console.log(`${this.color} 汪汪汪`);
}
}
// 這就是一個動物工廠
class Animal {
createAnimal(type: name) {
switch (type) {
case 'cat':
return new Cat();
case 'dog':
return new Dog();
}
}
}
const animal = new Animal();
const cat = animal.createAnimal('cat');
const dog = animal.createAnimal('dog');
cat.bark()
dog.bark()
複製代碼
abstract class Feature {
abstract color: string;
abstract bark(): void;
}
// 枚舉可使用的動物類型
enum animalType {
'cat',
'dog'
}
// 子類繼承抽象類 Feature
// 這樣咱們就能夠建立白色叫聲喵喵喵的貓了
class Cat extends Feature {
color = "白色";
bark() {
console.log(`${this.color} 喵喵喵`);
}
}
// 建立 Dog 類
class Dog extends Feature {
color = "黑色";
bark() {
console.log(`${this.color} 汪汪汪`);
}
}
// 這就是一個動物工廠
class Animal {
createAnimal(type: animalType) {
switch (type) {
case animalType.dog:
return new Cat();
case animalType.dog:
return new Dog();
}
}
}
const animal = new Animal();
const cat = animal.createAnimal(animalType.cat);
const dog = animal.createAnimal(animalType.dog);
cat.bark()
dog.bark()
複製代碼
享元模式,運用共享技術,有效地支持大量的細粒度的對象,以免對象之間擁有相同內容而形成多餘的性能開銷。
享元(flyweight)模式的主要做用:性能優化,當系統建立過多類似的對象而致使內存佔用太高,能夠採用這種設計模式進行優化。
享元模式將對象的屬性區分爲內部狀態與外部狀態,內部狀態在建立的時候賦值,外部狀態在實際須要用到的時候進行動態賦值
對於內部狀態和外部狀態的區分,有幾點:
咱們要建立 100 個大小相同顏色不一樣的 div。
interface Div {
width: number;
height: number;
color: string;
}
const divStore: Div[] = [];
class CreateDiv {
public width = 100;
public height = 100;
public color = this.randomColor()
// 隨機顏色
private randomColor () {
const color = ['red', 'green', 'blue', 'white', 'black'];
return color[Math.floor(Math.random() * color.length)];
}
}
let count = 100;
while (count--) {
const innerDiv = new CreateDiv();
divStore.push(innerDiv);
}
const sizeof = require('object-sizeof')
console.log(sizeof(divStore)) // 5688
複製代碼
// 將 div 屬性設置成內部跟外部兩部分
interface Div {
outer: {
width: number;
height: number;
};
innter: {
color: string;
};
}
// 用來儲存 Div
const divStore: Div[] = [];
// 建立外部 div 類
class CreateOuterDiv {
width: number = 100;
height: number = 100;
}
class CreateInnerDiv {
public color = this.randomColor()
// 隨機顏色
private randomColor () {
const color = ['red', 'green', 'blue', 'white', 'black'];
return color[Math.floor(Math.random() * color.length)];
}
}
// 建立外部 div
const outerDiv = new CreateOuterDiv();
let innerDiv: number;
let count = 100;
while (count--) {
// 建立內部 div
innerDiv = new CreateInnerDiv();
divStore.push({
outer: outerDiv,
innter: innerDiv
});
}
const sizeof = require('object-sizeof')
// 由於這個方法會把引用的對象也所有算一遍,因此咱們拆開來算
// 驗證:100 * (innerDiv + outerDiv)= 5400 與上面算的 5688 很接近,能夠認爲這個方法是準確的
console.log(100 * (sizeof(innerDiv) + sizeof(outerDiv))) // 5400
// 100 * innerDiv + outerDiv = 1638
console.log(100 * sizeof(innerDiv) + sizeof(outerDiv)) // 1638
複製代碼
從上面的計算結果來看減小了很大的內存,由於 divStore 數組對象中 outerDiv 其實只有一個,都是它的引用而已。咱們的內存佔用是 100 * innerDiv + outerDiv,而不使用享元模式的空間是 100 * (innerDiv + outerDiv)
定義一系列的算法, 把它們一個個封裝起來, 而且使它們可相互替換。
優勢
缺點
在vue中有一個合併選項策略 optionMergeStrategies
,它的功能就是把選項添加一些策略,能夠達到咱們對選項數據操做的目的
官方例子,將選項 _my_option
添加策略,讓它的值加一
Vue.config.optionMergeStrategies._my_option = function (parent, child, vm) {
return child + 1
}
const Profile = Vue.extend({
_my_option: 1
})
// Profile.options._my_option = 2
複製代碼
咱們來簡單實現一下這個合併選項策略
// 策略模式 store
const optionMergeStrategies: { [prop: string]: any } = {};
// 給 _my_option 添加策略
optionMergeStrategies._my_option = function(value) {
return value + 1
}
// 聲明 data
const data = {
// 添加策略
_my_option: 1,
// 未添加策略
option: 1
};
// 響應式
function reactive (data) {
const hander = {
get(target, key, value) {
const v = Reflect.get(target, key, value);
// 此屬性存在策略
if (typeof optionMergeStrategies[key] === 'function') {
return optionMergeStrategies[key](v)
}
return v
}
};
return new Proxy(data, hander);
}
const proxy = reactive(data);
// 測試是否添加了響應
proxy._my_option = 10
proxy.option = 10
console.log(proxy._my_option, proxy.option); // 11 10
複製代碼
這樣你就能夠作更多的事情了,好比驗證手機號,郵箱等等,不再用寫不少的 if else 了,並且你也能夠隨時更換策略。符合了設計模式的開閉原則。
當對象間存在一對多關係時,則使用觀察者模式(Observer Pattern)。好比,當一個對象被修改時,則會自動通知它的依賴對象。觀察者模式屬於行爲型模式。
優勢
缺點
好比公衆號,有多我的訂閱,天天定時發送公衆號文章
// 公衆號訂閱者
abstract class Persen {
abstract update(): void;
protected subject: Subject;
}
// 狀態
type state = 'await' | 'publish'
// 依賴
class Subject {
private _state: state = 'await'
// 依賴集合
subs: Persen[] = [];
// 防止頻繁設置狀態
lock = false
// 設置狀態,若是是發佈狀態的話,就發佈文章
set state(state: state) {
// 鎖上以後就不能設置狀態了,只有鎖解開後才能夠設置狀態
if (this.lock || (this._state = state) === 'await') return;
this.lock = true;
Promise.resolve().then(() => {
this.notify();
this.lock = false;
});
}
// 得到當前狀態
get state(): state {
return this._state
}
// 添加訂閱
attach(persen: Persen) {
this.subs.push(persen)
}
// 通知更新
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
// 建立一個 Tom
class Tom extends Persen {
constructor(subject: Subject) {
super();
subject.attach(this)
}
update() {
console.log('通知到了 Tom');
}
}
// 建立一個 Jick
class Jick extends Persen {
constructor(subject: Subject) {
super();
subject.attach(this)
}
update() {
console.log('通知到了 Jick');
}
}
// 實例化依賴
const subject = new Subject()
// Tom Jick 訂閱公衆號
new Tom(subject)
new Jick(subject)
// 由於設置了 lock 因此在一次 event loop 只會執行一次
subject.state = 'publish'
subject.state = 'await'
console.log(subject.state) // publish
subject.state = 'publish'
setTimeout(() => {
subject.state = 'publish'
}, 1000)
// 通知到了 Tom
// 通知到了 Jick
// 一秒後...
// 通知到了 Tom
// 通知到了 Jick
複製代碼
裝飾器模式(Decorator Pattern)容許向一個現有的對象添加新的功能,同時又不改變其結構。這種類型的設計模式屬於結構型模式,它是做爲現有的類的一個包裝。
這種模式建立了一個裝飾類,用來包裝原有的類,並在保持類方法簽名完整性的前提下,提供了額外的功能。
優勢
缺點
好比咱們要點擊一個按鈕,可是這個按鈕點擊時咱們想給他加上埋點並作一些登錄的邏輯
我這裏使用了 es7
的語法糖,固然不用語法糖也能夠作,可是我以爲用的話更簡潔一些
// Button 類,內部有一個 click 方法
// 對click方法作了兩個修飾
// 一個是添加埋點,一個是登錄
class Button {
@BuridDecorator
@LoginDecorator
click() {
console.log('點擊 dom')
}
}
// 登錄邏輯的裝飾器
function LoginDecorator(target, name, descriptor) {
const oriFun = target[name]
descriptor.value = async function() {
const code = await Login();
if (code === 0) {
console.log('登錄成功')
oriFun.call(this, ...arguments)
}
}
}
// 設置埋點的裝飾器
function BuridDecorator(target, name, descriptor) {
console.log(`${name} 方法添加了一個埋點`)
}
// 登錄邏輯
async function Login () {
return new Promise((resolve, reject)=> {
setTimeout(() => {
resolve(0)
}, 1000)
})
}
// 點擊按鈕
const btn = new Button()
btn.click();
// click 方法添加了一個埋點
// 登錄成功
// 點擊 dom
複製代碼
適配器模式(Adapter Pattern)是做爲兩個不兼容的接口之間的橋樑。這種類型的設計模式屬於結構型模式,它結合了兩個獨立接口的功能。
優勢
缺點
舉一個例子,你要寫一個頁面兼容各個端的小程序,那麼你就須要根據環境調用不一樣小程序的 sdk 方法。好比在支付寶中有一個 zhifubaoShare
的分享方法,在微信中有一個 weixinShare
的分享方法。(固然一個sdk還有不少方法,咱們只拿分享來舉例子)可是咱們在工做中其實只但願調用一個 share
方法就能實現不一樣端的分享。下面咱們用適配器模式來作一個 Adapter
適配器。
// =============== 定義接口與類型 ==============================
// 支付寶接口
interface ZhifubaoInerface {
zhifubaoShare(): void;
}
// 微信接口
interface WeixinInterface {
weixinShare(): void;
}
// adapter 接口
interface AdapterInterface {
share(): void;
}
// 合併全部 sdk 類型
interface MergeSdk extends ZhifubaoInerface, WeixinInterface {}
// 支持的平臺類型
type platform = 'weixin' | 'zhifubao';
// =============== 代碼邏輯實現 ==============================
// 微信 sdk 類實現
class WeixinSdk implements WeixinInterface {
weixinShare() {
console.log('微信分享');
}
}
// 支付寶 sdk 類實現
class ZhifubaoSdk implements ZhifubaoInerface {
zhifubaoShare() {
console.log('支付寶分享');
}
}
// adapter 類實現
class Adapter implements AdapterInterface {
constructor() {
this.sdk = this.getPlatfromSdk();
}
// 掛載 sdk
private sdk: MergeSdk;
// 根據 ua 獲取到平臺
private getPlatform(): platform {
// 默認寫了 weixin
return 'weixin';
}
// 將全部 sdk 方法放進一個 map 裏
private getPlatfromSdk() {
const map = {
weixin: WeixinSdk,
zhifubao: ZhifubaoSdk
};
const platform = this.getPlatform();
return new map[platform]() as MergeSdk;
}
// 分享功能
// 實際項目中還有參數的問題,這裏爲了代碼的簡潔就不寫了
public share() {
const platform = this.getPlatform();
switch (platform) {
case 'weixin':
this.sdk.weixinShare();
break;
case 'zhifubao':
this.sdk.zhifubaoShare();
break;
default:
console.log('此方法不存在');
}
}
}
const adapter = new Adapter();
// 由於咱們默認設置了 weixin 平臺
adapter.share(); // 微信分享
複製代碼
luoxue2479
回覆加羣便可