《設計模式》這樣學就對了!
李運通,微醫前端技術部前端工程師。最怕你一生碌碌無爲,還安慰自己平凡可貴。
設計模式解決什麼痛點?
它是解決特定問題的一系列套路,是前輩們的代碼設計經驗的總結,具有一定的普遍性,可以反覆使用。其目的是爲了
提高代碼的可複用性、可讀性、可維護性
。
設計模式的本質是面向對象設計原則的實際運用,是對類的封裝性、繼承性和多態性以及類的關聯關係和組合關係的充分理解。
不要重複造輪子
。
什麼是面向對象編程
-
面向對象編程
是一種編程範式或編程風格。它以類或對象作爲組織代碼的基本單元,並將封裝、抽象、繼承、多態四大特性,作爲代碼設計和實現的基石。 -
面向對象編程語言
是支持類或對象的語法機制,並有現成的語法機制,能方便的實現面向對象編程四大特性的編程語言 -
面向對象開發
包括面向對象分析 OOA、面向對象設計 OOD、面向對象編程 OOP
10 大設計原則
1. 單一職責原則 SRP
實現類要職責單一
:如果一段代碼塊(函數 類 模塊)負責多個功能,那麼當 A 功能需求發生改變的時候改動了代碼,就有可能導致 B 功能出現問題,所以一段代碼塊只應該負責一個職責。
2. 開放 - 封閉原則 OCP
要對擴展開放,對修改關閉
:通過修改老代碼來實現新功能可能導致老模塊出現 BUG,所以我們應該通過開發新代碼塊來實現新功能
3. 里氏替換原則 LSP
不要破壞繼承體系
:程序中的子類應該可以替換父類出現的任何地方並保持預期不變。所以子類儘量不要改變父類方法的預期行爲
4. 接口隔離原則 ISP
設計接口的時候要精簡單一
:當類 A 只需要接口 B 中的部分方法時,因爲實現接口需要實現其所有的方法,於是就造成了類 A 多出了部分不需要的代碼。這時應該將 B 接口拆分,將類 A 需要和不需要的方法隔離開來
5. 依賴倒置原則 DIP
面向接口編程
:抽象不應該依賴細節,細節應該依賴於抽象。核心是面向接口編程,我們應該依賴於抽象接口,而不是具體的接口實現類或具體的對象
注意:上面的 SOLID 又稱爲 5 大設計原則
6. 最少知識原則 (迪米特原則)LOD
降低耦合度
:一個類或對象應該對其它對象保持最少的瞭解。只與直接的朋友 (耦合) 通信。
7. 組合 / 聚合複用原則 CRP
多用組合少用繼承
:儘可能通過組合已有對象(借用他們的能力)來實現新的功能,而不是使用繼承來獲取這些能力
8. 不要重複你自己 DRY
功能語義重複應該合併,代碼執行重複應該刪減,代碼邏輯重複但語義不同應該保留
9. 儘量保持簡單 KISS
儘可能使用簡單可讀性高的代碼實現功能,而不用邏輯複雜、實現難度高、可讀性差的方式
10. 不要過度設計你暫時用不到的邏輯 YAGNI
不要過度優化、不要過度預留擴展點、不要設計同事看不懂的代碼。
23 種設計模式速記
-
速記:5+7=11
-
5 種創建型
-
7 種結構型
-
11 種行爲型
-
創建型:
抽工單建原型
-
抽象工廠、工廠、單例、建造者、原型
-
結構型:
橋代理裝飾適配器,享元組合成門面
-
橋接、代理、裝飾器、適配器、享元、組合、門面(外觀)
-
行爲型:
觀察模板迭代的狀態,命令中介解釋職責鏈,訪問策略備忘錄
-
觀察者、模板、迭代、狀態、命令、中介者、解釋器、職責鏈、訪問者、策略、備忘錄
創建型設計模式
封裝對象創建過程,將對象的創建和使用解耦
單例模式
應用場景
處理資源訪問衝突、用來創建全局唯一類
解決方案
-
懶漢式:用到的時候才創建(場景:不一定需要用到、創建代價大、延遲加載、需要快速啓動系統)
-
餓漢式:系統啓動時創建(場景:必定用到、臨時創建影響響應速度)
-
多例:同一類的固定個數相同實例
工廠模式
應用場景
用來創建繼承同一父類、實現同一接口的子類
對象,由給定的類型參數創建具體的對象。
解決方案
enum HelloType {
A,
B
}
interface Hello {
sayHello()
}
class A implements Hello {
sayHello() {
console.log('A');
}
}
class B implements Hello {
sayHello() {
console.log('B');
}
}
class HelloFactory {
static list = new Map<HelloType, Hello>([
[HelloType.A, new A()],
[HelloType.B, new B()]
])
static getHello(type: HelloType) {
return HelloFactory.list.get(type)
}
}
// test
HelloFactory.getHello(HelloType.A).sayHello()
HelloFactory.getHello(HelloType.B).sayHello()
抽象工廠模式
應用場景
繼承同一父類、實現同一接口的子類
對象,由給定的多個類型參數創建具體的對象。
解決方案
enum Type {
A,
B
}
enum Occupation {
TEACHER,
STUDENT
}
interface Hello {
sayHello()
}
class TA implements Hello {
sayHello() {
console.log('Teacher A say hello')
}
}
class TB implements Hello {
sayHello() {
console.log('Teacher B say hello')
}
}
class SA implements Hello {
sayHello() {
console.log('Student A say hello')
}
}
class SB implements Hello {
sayHello() {
console.log('Student B say hello')
}
}
class AFactory {
static list = new Map<Occupation, Hello>([
[Occupation.TEACHER, new TA()],
[Occupation.STUDENT, new SA()]
])
static getHello(occupation: Occupation) {
return AFactory.list.get(occupation)
}
}
class BFactory {
static list = new Map<Occupation, Hello>([
[Occupation.TEACHER, new TB()],
[Occupation.STUDENT, new SB()]
])
static getHello(occupation: Occupation) {
return BFactory.list.get(occupation)
}
}
class HelloFactory {
static list = new Map<Type, AFactory | BFactory>([
[Type.A, AFactory],
[Type.B, BFactory]
])
static getType(type: Type) {
return HelloFactory.list.get(type)
}
}
// test
HelloFactory.getType(Type.A).getHello(Occupation.TEACHER).sayHello()
HelloFactory.getType(Type.A).getHello(Occupation.STUDENT).sayHello()
HelloFactory.getType(Type.B).getHello(Occupation.TEACHER).sayHello()
HelloFactory.getType(Type.B).getHello(Occupation.STUDENT).sayHello()
建造者模式
應用場景
-
創建時有很多必填參數需要驗證
-
創建時參數求值有先後順序、相互依賴
-
創建有很多步驟,全部成功才能創建對象
解決方案
class Programmer {
age: number
username: string
color: string
area: string
constructor(p) {
this.age = p.age
this.username = p.username
this.color = p.color
this.area = p.area
}
toString() {
console.log(this)
}
}
class Builder {
age: number
username: string
color: string
area: string
build() {
if (this.age && this.username && this.color && this.area) {
return new Programmer(this)
} else {
throw new Error('缺少信息')
}
}
setAge(age: number) {
if (age > 18 && age < 36) {
this.age = age
return this
} else {
throw new Error('年齡不合適')
}
}
setUsername(username: string) {
if (username !== '小明') {
this.username = username
return this
} else {
throw new Error('小明不合適')
}
}
setColor(color: string) {
if (color !== 'yellow') {
this.color = color
return this
} else {
throw new Error('yellow不合適')
}
}
setArea(area: string) {
this.area = area
return this
}
}
// test
const p = new Builder()
.setAge(20)
.setUsername('小紅')
.setColor('red')
.setArea('hz')
.build()
.toString()
原型模式
應用場景
-
原型模式是基於已有的對象克隆數據,而不是修改原型鏈!
-
創建對象的代價太大,而同類的不同實例對象屬性值基本一致。通過原型克隆的方式節約資源
-
不可變對象通過淺克隆實現
-
可變對象通過深克隆實現,深克隆佔用資源多
-
同一對象不同時間版本,可以對比沒變化的淺克隆,變化的深克隆,然後新版本替換舊版本。
結構型設計模式
總結了一些類或對象組合在一起的經典結構,這些經典結構可以解決特定應用場景的問題,將類或對象的結構和使用解耦
橋接模式
應用場景
-
將抽象和實現解耦,讓它們可以獨立變化
-
一個類存在多個獨立變化的維度,我們通過組合的方式,讓多個維度可以獨立進行擴展
-
非常類似於組合優於繼承原則
解決方案
enum MsgLevel {
ERROR,
WARN,
}
enum MsgType {
EMAIL,
PHONE
}
interface MsgContent {
content()
}
class ErrorMsg implements MsgContent {
content() {
return 'ERROR'
}
}
class WarnMsg implements MsgContent {
content() {
return 'WARN'
}
}
interface MsgSender {
send()
}
class PhoneSend implements MsgSender {
msgContent: MsgContent
constructor(msgContent: MsgContent) {
this.msgContent = msgContent
}
send() {
console.log(`phone send ${this.msgContent.content()}`)
}
}
class EmailSend implements MsgSender {
msgContent: MsgContent
constructor(msgContent: MsgContent) {
this.msgContent = msgContent
}
send() {
console.log(`email send ${this.msgContent.content()}`)
}
}
// test 此處還可以做成map結構繼續優化(略)
new PhoneSend(new WarnMsg()).send()
new PhoneSend(new ErrorMsg()).send()
new EmailSend(new WarnMsg()).send()
new EmailSend(new ErrorMsg()).send()
代理模式
應用場景
-
給原類添加非功能性需求,爲了將代碼與原業務解耦
-
業務系統的非功能性需求開發:監控、統計、鑑權、限流、日誌、緩存
解決方案
- 通過繼承實現(不推薦)
class User{
login(){
console.log('user login...')
}
}
class UserProxy extends User{
login() {
console.log('login before')
super.login()
console.log('login after')
}
}
- 通過接口實現(推薦)
interface Login {
login()
}
class User implements Login {
login() {
console.log('user login...')
}
}
class UserProxy implements Login {
user = new User()
login() {
console.log('login before')
this.user.login()
console.log('login after')
}
}
裝飾器模式
應用場景
-
裝飾器類是對原始功能的增強
-
裝飾器類和原始類繼承同樣的父類,這樣我們可以對原始類嵌套多個裝飾器類
-
主要解決繼承關係過於複雜的問題,通過組合來替代繼承
-
可以通過對原始類嵌套使用多個裝飾器
解決方案
- 通過 AOP 實現
Function.prototype.before = function (beforeFn) {
return (...arg) => {
beforeFn(...arg);
return this(...arg);
}
};
Function.prototype.after = function (afterFn) {
return (...arg) => {
const result = this(...arg);
afterFn(...arg);
return result;
}
};
function ImportEvent1 {
console.log('重要的事情說三遍 1')
}
function ImportEvent2 {
console.log('重要的事情說三遍 2')
}
function ImportEvent3 {
console.log('重要的事情說三遍 3')
}
// test
ImportEvent2.before(ImportEvent1).after(ImportEvent3)()
適配器模式
應用場景
-
適配器模式用於補救設計上的缺陷,將不兼容的接口變得兼容
-
封裝有缺陷的接口設計
-
統一多個類的接口設計
-
替換依賴的外部系統
-
兼容老版本接口
-
適配不同格式的數據
解決方案
-
原接口方法不多,類適配器和對象適配器都可以
-
如果原類方法很多,並且和目標接口差異小,用類適配器減少代碼量
-
如果原類方法很多,並且和目標接口差異大,用對象適配器,組合優於繼承
// 目標接口格式
interface ITarget {
f1()
f2()
f3()
}
// 原有類與目標接口不兼容
class Origin {
fa() {
}
fb() {
}
f3() {
}
}
// 使用適配器來兼容
class Adaptor implements ITarget {
origin = new Origin()
f1() {
this.origin.fa()
}
f2() {
this.origin.fb()
}
f3() {
this.origin.f3()
}
}
享元模式
應用場景
- 共享的單元。複用對象,節省內存,前提是享元對象是不可變對象 (初始化之後不再改變)。
解決方案
- 比如網上象棋遊戲有 1000 個房間,每個房間有 1 個棋盤,棋盤當前狀態 (棋子位置) 各不相同,但棋子的大小、顏色、名字是相同且固定的,可以設計成享元
組合模式
應用場景
將一組對象組織成樹形結構,以表示一種 “部分 - 整體” 的層次結構。組合模式讓客戶端可以統一單個對象和組合對象的處理邏輯(遞歸遍歷)
解決方案
abstract class FileSystemNode {
path: string
abstract getFilesCount()
abstract getFilesSize()
}
class FileNode extends FileSystemNode {
constructor(path) {
super();
this.path = path
}
getFilesCount() {
return 1
}
getFilesSize() {
return require(this.path).length
}
}
class Directory extends FileSystemNode {
subNodes = []
constructor(path) {
super();
this.path = path
}
getFilesCount() {
return this.subNodes.reduce(item => item.getCount(), 0)
}
getFilesSize() {
return this.subNodes.reduce(item => item.getSize(), 0)
}
}
門面 (外觀) 模式
應用場景
-
將多個後端接口請求合併爲一個(冗餘接口),提高響應速度,解決性能問題
-
通過封裝細粒度接口,提供組合各個細粒度接口的高層次接口,來提高接口的易用性
行爲型設計模式
總結了一些類或對象交互的經典方式,將該行爲相關的類或對象解耦
觀察者模式
應用場景
-
將觀察者與被觀察者解耦
-
發佈訂閱模式有發佈訂閱調度中心 (中間商),觀察者模式沒有!
解決方案
// 目標對象
class Subject {
observerList: Observer[]
constructor() {
this.observerList = [];
}
addObserver(observer) {
this.observerList.push(observer);
}
notify() {
this.observerList.forEach((observer) => {
observer.update();
});
}
}
// 觀察者
class Observer {
cb: Function
constructor(cb: Function) {
if (typeof cb === "function") {
this.cb = cb;
} else {
throw new Error("Observer構造器必須傳入函數類型!");
}
}
update() {
this.cb();
}
}
// test
const observerCallback = function () {
console.log("我被通知了");
};
const observer = new Observer(observerCallback);
const subject = new Subject();
subject.addObserver(observer);
subject.notify();
模板模式
應用場景
-
在一個方法裏定義一個算法 (業務邏輯) 骨架,並將某些步驟推遲到子類中實現。模板方法模式可以讓子類在不改變算法整體結構的情況下,重新定義算法中的某些步驟。
-
複用 擴展
解決方案
abstract class Drinks {
firstStep() {
console.log('燒開水')
}
abstract secondStep()
thirdStep() {
console.log('倒入杯子')
}
abstract fourthStep()
drink() {
this.firstStep()
this.secondStep()
this.thirdStep()
this.fourthStep()
}
}
class Tea extends Drinks {
secondStep() {
console.log('浸泡茶葉')
}
fourthStep() {
console.log('加檸檬')
}
}
class Coffee extends Drinks {
secondStep() {
console.log('沖泡咖啡')
}
fourthStep() {
console.log('加糖')
}
}
// test
const tea = new Tea()
tea.drink()
const coffee = new Coffee()
coffee.drink()
策略模式
應用場景
-
定義一族算法族,將每個算法分別封裝起來,讓他們可以相互替換。
-
避免冗長的 if-else 或 switch 分支判斷
解決方案
enum StrategyType {
S,
A,
B
}
const strategyFn = {
'S': function (salary: number) {
return salary * 4
},
'A': function (salary: number) {
return salary * 3
},
'B': function (salary: number) {
return salary * 2
}
}
const calculateBonus = function (level: StrategyType, salary: number) {
return strategyFn[level](salary)
}
calculateBonus(StrategyType.A, 10000) // 30000
職責鏈模式
應用場景
-
多個處理器 ABC 依次處理同一個請求,形成一個鏈條,當某個處理器能處理這個請求,就不會繼續傳遞給後續處理器了
-
過濾器 攔截器 處理器
解決方案
const order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log("500 元定金預購, 得到 100 元優惠券");
return true;
} else {
return false;
}
};
const order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log("200 元定金預購, 得到 50 元優惠券");
return true;
} else {
return false;
}
};
const orderCommon = function (orderType, pay, stock) {
if ((orderType === 3 || !pay) && stock > 0) {
console.log("普通購買, 無優惠券");
return true;
} else {
console.log("庫存不夠, 無法購買");
return false;
}
};
class chain {
fn: Function
nextFn: Function
constructor(fn: Function) {
this.fn = fn;
this.nextFn = null;
}
setNext(nextFn) {
this.nextFn = nextFn
}
init(...arguments) {
const result = this.fn(...arguments);
if (!result && this.nextFn) {
this.nextFn.init(...arguments);
}
}
}
const order500New = new chain(order500);
const order200New = new chain(order200);
const orderCommonNew = new chain(orderCommon);
order500New.setNext(order200New);
order200New.setNext(orderCommonNew);
order500New.init(3, true, 500); // 普通購買, 無優惠券
狀態模式
應用場景
- 將事物內部的每個狀態分別封裝成類, 內部狀態改變會產生不同行爲
解決方案
class weakLight {
light: Light
constructor(light: Light) {
this.light = light
}
press() {
console.log('打開強光')
this.light.setState(this.light.strongLight)
}
}
class strongLight {
light: Light
constructor(light: Light) {
this.light = light
}
press() {
console.log('關燈')
this.light.setState(this.light.offLight)
}
}
class offLight {
light: Light
constructor(light: Light) {
this.light = light
}
press() {
console.log('打開弱光')
this.light.setState(this.light.weakLight)
}
}
class Light {
weakLight: weakLight
strongLight: strongLight
offLight: offLight
currentState: offLight | weakLight | strongLight //當前狀態: 默認關燈狀態
constructor() {
this.weakLight = new weakLight(this)
this.strongLight = new strongLight(this)
this.offLight = new offLight(this)
this.currentState = this.offLight
}
press() {
this.currentState.press()
}
setState(state) {
this.currentState = state
}
}
// test
const light = new Light()
light.press()
light.press()
light.press()
light.press()
light.press()
light.press()
迭代器模式
應用場景
- 遍歷集合對象
訪問者模式
應用場景
- 允許一個或多個操作應用到一組對象上,解耦操作和對象本身
備忘錄模式
應用場景
- 在不違背封裝原則的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態,以便之後恢復對象爲先前的狀態
解決方案
class Programmer {
age: number
username: string
color: string
area: string
constructor(p) {
this.age = p.age
this.username = p.username
this.color = p.color
this.area = p.area
}
// 創建一個快照
createSnapshot() {
return {
age: this.age,
username: this.username,
color: this.color,
area: this.area
}
}
// 通過快照恢復對象狀態
restoreSnapshot(snapshot: Programmer) {
this.age = snapshot.age
this.username = snapshot.username
this.color = snapshot.color
this.area = snapshot.area
}
}
命令模式
應用場景
- 命令模式的主要作用和應用場景,是用來控制命令的執行,比如,異步、延遲、排隊執行命令、撤銷重做命令、存儲命令、給命令記錄日誌等。將命令的發起者和執行者解耦。
解決方案
interface Command {
execute()
}
class closeDoorCommand implements Command {
execute() {
console.log('close door');
}
}
class openPcCommand implements Command {
execute() {
console.log('open pc');
}
}
class openQQCommand implements Command {
execute() {
console.log('login qq');
}
}
class CommandManager {
commandList: Command[] = []
addCommand(command: Command) {
this.commandList.push(command)
}
execute() {
this.commandList.forEach(command => {
command.execute()
})
}
}
//test
const commandManager = new CommandManager();
commandManager.addCommand(new closeDoorCommand());
commandManager.addCommand(new openPcCommand());
commandManager.addCommand(new openQQCommand());
commandManager.execute();
解釋器模式
應用場景
給定一個語言,定義它的文法表示,並定義一個解釋器,這個解釋器使用該標識來解釋語言中的句子
中介模式
應用場景
- 中介模式的設計思想跟中間層很像,通過引入中介這個中間層,將一組對象之間的交互關係(依賴關係)轉換成一對多(星狀關係)。原本一個對象要跟 n 個對象交互,現在只需要跟一箇中介對象交互,從而最小化對象間的交互關係,降低了代碼複雜度,提高了代碼的可讀性和可維護性。
如何評價代碼的質量?
-
可讀性、可擴展性、可維護性、可複用性、可測試性...
-
高內聚低耦合
-
善戰者無赫赫之功善醫者無煌煌之名,大智若愚大巧若拙,真正的好代碼並不是用了多少厲害的技術與奇技淫巧,而是看盡人世繁華後的返璞歸真,寥寥幾筆實現了功能的同時卻沒有任何個人風格的痕跡,小白都能看得懂的代碼纔是好代碼。
怎樣形成長期記憶?
-
想辦法把零散的知識點串聯起來記憶
-
自頂向下形成金字塔結構記憶
-
編成關鍵字口訣記憶
-
得意忘形
-
將知識的精華枝幹提取出來強化記憶,去粗取精
-
學而不思則罔,思而不學則殆
-
深度思考能將他人的知識真正轉化成自己的
-
學而時習之,不亦說乎
-
第一次學會只是腦海中的短時記憶,
需要多次複習強化才能形成長期記憶
注意事項
知識是死的,而代碼是活的,不要用固化的設計模式實現硬套在活的業務邏輯裏。
能學以致用是我們的學習目標,但是如果寫出來的代碼同組的其他人都看不懂,更加影響項目的可維護性和開發效率。所以我們可以少用慎用,但是我們必須掌握其思想。
牢牢掌握設計模式,拿去面試、面試別人、組內分享還是可以震懾羣雄的,啊哈哈哈
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/I65GKW8Jp0T-CJCU6qcDxA