上期回顧
提升開發思維的設計模式(上)
2. 設計模式分類(23種設計模式)
2.13 組合模式(Composite Pattern)
-
將對象組合成樹形結構,以表示“整體-部分”的層次結構。
-
通過對象的多態表現,使得用戶對單個對象和組合對象的使用具有一致性。
class TrainOrder {create () {console.log('創建火車票訂單')}
}
class HotelOrder {create () {console.log('創建酒店訂單')}
}class TotalOrder {constructor () {this.orderList = []}addOrder (order) {this.orderList.push(order)return this}create () {this.orderList.forEach(item => {item.create()})return this}
}
// 可以在購票網站買車票同時也訂房間
let train = new TrainOrder()
let hotel = new HotelOrder()
let total = new TotalOrder()
total.addOrder(train).addOrder(hotel).create()
場景
-
表示對象-整體層次結構
-
希望用戶忽略組合對象和單個對象的不同,用戶將統一地使用組合結構中的所有對象(方法)
缺點
如果通過組合模式創建了太多的對象,那么這些對象可能會讓系統負擔不起。
2.14 原型模式(Prototype Pattern)
原型模式(prototype)是指用原型實例指向創建對象的種類,并且通過拷貝這些原型創建新的對象。
class Person {constructor(name) {this.name = name}getName() {return this.name}
}
class Student extends Person {constructor(name) {super(name)}sayHello() {console.log(`Hello, My name is ${this.name}`)}
}let student = new Student("xiaoming")
student.sayHello()
原型模式,就是創建一個共享的原型,通過拷貝這個原型來創建新的類,用于創建重復的對象,帶來性能上的提升。
2.15 策略模式(Strategy Pattern)
定義一系列的算法,把它們一個個封裝起來,并且使它們可以互相替換
<html>
<head><title>策略模式-校驗表單</title><meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body><form id = "registerForm" method="post" action="http://xxxx.com/api/register">用戶名:<input type="text" name="userName">密碼:<input type="text" name="password">手機號碼:<input type="text" name="phoneNumber"><button type="submit">提交</button></form><script type="text/javascript">// 策略對象const strategies = {isNoEmpty: function (value, errorMsg) {if (value === '') {return errorMsg;}},isNoSpace: function (value, errorMsg) {if (value.trim() === '') {return errorMsg;}},minLength: function (value, length, errorMsg) {if (value.trim().length < length) {return errorMsg;}},maxLength: function (value, length, errorMsg) {if (value.length > length) {return errorMsg;}},isMobile: function (value, errorMsg) {if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {return errorMsg;} }}// 驗證類class Validator {constructor() {this.cache = []}add(dom, rules) {for(let i = 0, rule; rule = rules[i++];) {let strategyAry = rule.strategy.split(':')let errorMsg = rule.errorMsgthis.cache.push(() => {let strategy = strategyAry.shift()strategyAry.unshift(dom.value)strategyAry.push(errorMsg)return strategies[strategy].apply(dom, strategyAry)})}}start() {for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {let errorMsg = validatorFunc()if (errorMsg) {return errorMsg}}}}// 調用代碼let registerForm = document.getElementById('registerForm')let validataFunc = function() {let validator = new Validator()validator.add(registerForm.userName, [{strategy: 'isNoEmpty',errorMsg: '用戶名不可為空'}, {strategy: 'isNoSpace',errorMsg: '不允許以空白字符命名'}, {strategy: 'minLength:2',errorMsg: '用戶名長度不能小于2位'}])validator.add(registerForm.password, [ {strategy: 'minLength:6',errorMsg: '密碼長度不能小于6位'}])validator.add(registerForm.phoneNumber, [{strategy: 'isMobile',errorMsg: '請輸入正確的手機號碼格式'}])return validator.start()}registerForm.onsubmit = function() {let errorMsg = validataFunc()if (errorMsg) {alert(errorMsg)return false}}</script>
</body>
</html>
場景例子
-
如果在一個系統里面有許多類,它們之間的區別僅在于它們的'行為',那么使用策略模式可以動態地讓一個對象在許多行為中選擇一種行為。
-
一個系統需要動態地在幾種算法中選擇一種。
-
表單驗證
優點
-
利用組合、委托、多態等技術和思想,可以有效的避免多重條件選擇語句
-
提供了對開放-封閉原則的完美支持,將算法封裝在獨立的strategy中,使得它們易于切換,理解,易于擴展
-
利用組合和委托來讓Context擁有執行算法的能力,這也是繼承的一種更輕便的代替方案
缺點
-
會在程序中增加許多策略類或者策略對象
-
要使用策略模式,必須了解所有的strategy,必須了解各個strategy之間的不同點,這樣才能選擇一個合適的strategy
2.16 享元模式(Flyweight Pattern)
運用共享技術有效地支持大量細粒度對象的復用。系統只使用少量的對象,而這些對象都很相似,狀態變化很小,可以實現對象的多次復用。由于享元模式要求能夠共享的對象必須是細粒度對象,因此它又稱為輕量級模式,它是一種對象結構型模式
let examCarNum = 0 // 駕考車總數
/* 駕考車對象 */
class ExamCar {constructor(carType) {examCarNum++this.carId = examCarNumthis.carType = carType ? '手動檔' : '自動檔'this.usingState = false // 是否正在使用}/* 在本車上考試 */examine(candidateId) {return new Promise((resolve => {this.usingState = trueconsole.log(`考生- ${ candidateId } 開始在${ this.carType }駕考車- ${ this.carId } 上考試`)setTimeout(() => {this.usingState = falseconsole.log(`%c考生- ${ candidateId } 在${ this.carType }駕考車- ${ this.carId } 上考試完畢`, 'color:#f40')resolve() // 0~2秒后考試完畢}, Math.random() * 2000)}))}
}/* 手動檔汽車對象池 */
ManualExamCarPool = {_pool: [], // 駕考車對象池_candidateQueue: [], // 考生隊列/* 注冊考生 ID 列表 */registCandidates(candidateList) {candidateList.forEach(candidateId => this.registCandidate(candidateId))},/* 注冊手動檔考生 */registCandidate(candidateId) {const examCar = this.getManualExamCar() // 找一個未被占用的手動檔駕考車if (examCar) {examCar.examine(candidateId) // 開始考試,考完了讓隊列中的下一個考生開始考試.then(() => {const nextCandidateId = this._candidateQueue.length && this._candidateQueue.shift()nextCandidateId && this.registCandidate(nextCandidateId)})} else this._candidateQueue.push(candidateId)},/* 注冊手動檔車 */initManualExamCar(manualExamCarNum) {for (let i = 1; i <= manualExamCarNum; i++) {this._pool.push(new ExamCar(true))}},/* 獲取狀態為未被占用的手動檔車 */getManualExamCar() {return this._pool.find(car => !car.usingState)}
}ManualExamCarPool.initManualExamCar(3) // 一共有3個駕考車
ManualExamCarPool.registCandidates([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) // 10個考生來考試
場景例子
-
文件上傳需要創建多個文件實例的時候
-
如果一個應用程序使用了大量的對象,而這些大量的對象造成了很大的存儲開銷時就應該考慮使用享元模式
優點
-
大大減少對象的創建,降低系統的內存,使效率提高。
缺點
-
提高了系統的復雜度,需要分離出外部狀態和內部狀態,而且外部狀態具有固有化的性質, 不應該隨著內部狀態的變化而變化,否則會造成系統的混亂
2.17 模板方法模式(Template Method Pattern)
模板方法模式由兩部分結構組成,第一部分是抽象父類,第二部分是具體的實現子類。通常在抽象父類中封裝了子類的算法框架,包括實現一些公共方法和封裝子類中所有方法的執行順序。子類通過繼承這個抽象類,也繼承了整個算法結構,并且可以選擇重寫父類的方法。
class Beverage {constructor({brewDrink, addCondiment}) {this.brewDrink = brewDrinkthis.addCondiment = addCondiment}/* 燒開水,共用方法 */boilWater() { console.log('水已經煮沸=== 共用') }/* 倒杯子里,共用方法 */pourCup() { console.log('倒進杯子里===共用') }/* 模板方法 */init() {this.boilWater()this.brewDrink()this.pourCup()this.addCondiment()}
}
/* 咖啡 */
const coffee = new Beverage({/* 沖泡咖啡,覆蓋抽象方法 */brewDrink: function() { console.log('沖泡咖啡') },/* 加調味品,覆蓋抽象方法 */addCondiment: function() { console.log('加點奶和糖') }
})
coffee.init()
場景例子
-
一次性實現一個算法的不變的部分,并將可變的行為留給子類來實現
-
子類中公共的行為應被提取出來并集中到一個公共父類中的避免代碼重復
優點
-
提取了公共代碼部分,易于維護
缺點
-
增加了系統復雜度,主要是增加了的抽象類和類間聯系
2.18 責任鏈模式(Chain of Responsibility)
使多個對象都有機會處理請求,從而避免請求的發送者和接受者之間的耦合關系,將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止
// 請假審批,需要組長審批、經理審批、總監審批
class Action {constructor(name) {this.name = namethis.nextAction = null}setNextAction(action) {this.nextAction = action}handle() {console.log( `${this.name} 審批`)if (this.nextAction != null) {this.nextAction.handle()}}
}let a1 = new Action("組長")
let a2 = new Action("經理")
let a3 = new Action("總監")
a1.setNextAction(a2)
a2.setNextAction(a3)
a1.handle()
場景例子
-
JS 中的事件冒泡
-
作用域鏈
-
原型鏈
優點
-
降低耦合度。它將請求的發送者和接收者解耦。
-
簡化了對象。使得對象不需要知道鏈的結構
-
增強給對象指派職責的靈活性。通過改變鏈內的成員或者調動它們的次序,允許動態地新增或者刪除責任
-
增加新的請求處理類很方便。
缺點
-
不能保證某個請求一定會被鏈中的節點處理,這種情況可以在鏈尾增加一個保底的接受者節點來處理這種即將離開鏈尾的請求。
-
使程序中多了很多節點對象,可能再一次請求的過程中,大部分的節點并沒有起到實質性的作用。他們的作用僅僅是讓請求傳遞下去,從性能當面考慮,要避免過長的職責鏈到來的性能損耗。
2.19 命令模式(Command)
將一個請求封裝成一個對象,從而讓你使用不同的請求把客戶端參數化,對請求排隊或者記錄請求日志,可以提供命令的撤銷和恢復功能。
// 接收者類
class Receiver {execute() {console.log('接收者執行請求')}}// 命令者
class Command { constructor(receiver) {this.receiver = receiver}execute () { console.log('命令');this.receiver.execute()}
}
// 觸發者
class Invoker { constructor(command) {this.command = command}invoke() { console.log('開始')this.command.execute()}
}// 倉庫
const warehouse = new Receiver();
// 訂單
const order = new Command(warehouse);
// 客戶
const client = new Invoker(order);
client.invoke()
優點
-
對命令進行封裝,使命令易于擴展和修改
-
命令發出者和接受者解耦,使發出者不需要知道命令的具體執行過程即可執行
缺點
-
使用命令模式可能會導致某些系統有過多的具體命令類。
2.20 備忘錄模式(Memento)
在不破壞封裝性的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態。這樣以后就可將該對象恢復到保存的狀態。
//備忘類
class Memento{constructor(content){this.content = content}getContent(){return this.content}
}
// 備忘列表
class CareTaker {constructor(){this.list = []}add(memento){this.list.push(memento)}get(index){return this.list[index]}
}
// 編輯器
class Editor {constructor(){this.content = null}setContent(content){this.content = content}getContent(){return this.content}saveContentToMemento(){return new Memento(this.content)}getContentFromMemento(memento){this.content = memento.getContent()}
}//測試代碼let editor = new Editor()
let careTaker = new CareTaker()editor.setContent('111')
editor.setContent('222')
careTaker.add(editor.saveContentToMemento())
editor.setContent('333')
careTaker.add(editor.saveContentToMemento())
editor.setContent('444')console.log(editor.getContent()) //444
editor.getContentFromMemento(careTaker.get(1))
console.log(editor.getContent()) //333editor.getContentFromMemento(careTaker.get(0))
console.log(editor.getContent()) //222
場景例子
-
分頁控件
-
撤銷組件
優點
-
給用戶提供了一種可以恢復狀態的機制,可以使用戶能夠比較方便地回到某個歷史的狀態
缺點
-
消耗資源。如果類的成員變量過多,勢必會占用比較大的資源,而且每一次保存都會消耗一定的內存。
2.21 中介者模式(Mediator)
解除對象與對象之間的緊耦合關系。增加一個中介者對象后,所有的 相關對象都通過中介者對象來通信,而不是互相引用,所以當一個對象發生改變時,只需要通知 中介者對象即可。中介者使各對象之間耦合松散,而且可以獨立地改變它們之間的交互。中介者 模式使網狀的多對多關系變成了相對簡單的一對多關系(類似于觀察者模式,但是單向的,由中介者統一管理。)
class A {constructor() {this.number = 0}setNumber(num, m) {this.number = numif (m) {m.setB()}}
}
class B {constructor() {this.number = 0}setNumber(num, m) {this.number = numif (m) {m.setA()}}
}
class Mediator {constructor(a, b) {this.a = athis.b = b}setA() {let number = this.b.numberthis.a.setNumber(number * 10)}setB() {let number = this.a.numberthis.b.setNumber(number / 10)}
}let a = new A()
let b = new B()
let m = new Mediator(a, b)
a.setNumber(10, m)
console.log(a.number, b.number)
b.setNumber(10, m)
console.log(a.number, b.number)
場景例子
-
系統中對象之間存在比較復雜的引用關系,導致它們之間的依賴關系結構混亂而且難以復用該對象
-
想通過一個中間類來封裝多個類中的行為,而又不想生成太多的子類。
優點
-
使各對象之間耦合松散,而且可以獨立地改變它們之間的交互
-
中介者和對象一對多的關系取代了對象之間的網狀多對多的關系
-
如果對象之間的復雜耦合度導致維護很困難,而且耦合度隨項目變化增速很快,就需要中介者重構代碼
缺點
-
系統中會新增一個中介者對象,因 為對象之間交互的復雜性,轉移成了中介者對象的復雜性,使得中介者對象經常是巨大的。中介 者對象自身往往就是一個難以維護的對象。
2.22 解釋器模式(Interpreter)
給定一個語言, 定義它的文法的一種表示,并定義一個解釋器, 該解釋器使用該表示來解釋語言中的句子。
class Context {constructor() {this._list = []; // 存放 終結符表達式this._sum = 0; // 存放 非終結符表達式(運算結果)}get sum() {return this._sum;}set sum(newValue) {this._sum = newValue;}add(expression) {this._list.push(expression);}get list() {return [...this._list];}}class PlusExpression {interpret(context) {if (!(context instanceof Context)) {throw new Error("TypeError");}context.sum = ++context.sum;}}class MinusExpression {interpret(context) {if (!(context instanceof Context)) {throw new Error("TypeError");}context.sum = --context.sum;}}/** 以下是測試代碼 **/const context = new Context();// 依次添加: 加法 | 加法 | 減法 表達式context.add(new PlusExpression());context.add(new PlusExpression());context.add(new MinusExpression());// 依次執行: 加法 | 加法 | 減法 表達式context.list.forEach(expression => expression.interpret(context));console.log(context.sum);
優點
-
易于改變和擴展文法。
-
由于在解釋器模式中使用類來表示語言的文法規則,因此可以通過繼承等機制來改變或擴展文法
缺點
-
執行效率較低,在解釋器模式中使用了大量的循環和遞歸調用,因此在解釋較為復雜的句子時其速度慢
-
對于復雜的文法比較難維護
2.23 訪問者模式(Visitor)
表示一個作用于某對象結構中的各元素的操作。它使你可以在不改變各元素的類的前提下定義作用于這些元素的新操作。
// 訪問者
class Visitor {constructor() {}visitConcreteElement(ConcreteElement) {ConcreteElement.operation()}
}
// 元素類
class ConcreteElement{constructor() {}operation() {console.log("ConcreteElement.operation invoked"); }accept(visitor) {visitor.visitConcreteElement(this)}
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
element.accept(visitor)
場景例子
-
對象結構中對象對應的類很少改變,但經常需要在此對象結構上定義新的操作
-
需要對一個對象結構中的對象進行很多不同的并且不相關的操作,而需要避免讓這些操作"污染"這些對象的類,也不希望在增加新操作時修改這些類。
優點
-
符合單一職責原則
-
優秀的擴展性
-
靈活性
缺點
-
具體元素對訪問者公布細節,違反了迪米特原則
-
違反了依賴倒置原則,依賴了具體類,沒有依賴抽象。
-
具體元素變更比較困難
3. 總結
設計模式是為了可復用、可拓展、高性能軟件,前人給我們總結的寶貴經驗。
設計模式(Design Pattern)是前輩們對代碼開發經驗的總結,是解決特定問題的一系列套路。它不是語法規定,而是一套用來提高代碼可復用性、可維護性、可讀性、穩健性以及安全性的解決方案。
當然,軟件設計模式只是一個引導,在實際的軟件開發中,必須根據具體的需求來選擇。
4. 團隊介紹
「三翼鳥數字化技術平臺-ToC服務平臺」以用戶行為數據為基礎,利用推薦引擎為用戶提供“千人千面”的個性化推薦服務,改善用戶體驗,持續提升核心業務指標。通過構建高效、智能的線上運營系統,全面整合數據資產,實現數據分析-人群圈選-用戶觸達-后效分析-策略優化的運營閉環,并提供可視化報表,一站式操作提升數字化運營效率。