收藏給你的女朋友,這10 種常用設(shè)計模式
用自己通俗易懂的語言理解設(shè)計模式。
通過對每種設(shè)計模式的學習,更加加深了我對它的理解,也能在工作中考慮應(yīng)用場合。
成文思路:分析每種設(shè)計模式思想、抽離出應(yīng)用場景、對這些模式進行對比
此篇文章包含:修飾者模式(裝飾器)、單例模式、工廠模式、訂閱者模式、觀察者模式、代理模式
將不變的部分和變化的部分隔開是每個設(shè)計模式的主題。
單例模式
也叫單體模式,核心思想是確保一個類只對應(yīng)一個實例。
特點:
只允許一個例存在,提供全局訪問點,緩存初次創(chuàng)建的變量對象
排除全局變量,防止全局變量被重寫
可全局訪問
// 工廠模式和new模式實現(xiàn)單例模式
vue的安裝插件屬于單例模式
適用場景:適用于彈框的實現(xiàn), 全局緩存,一個單一對象。比如:彈窗,無論點擊多少次,彈窗只應(yīng)該被創(chuàng)建一次。
缺點:
防止全局變量被污染,看過多種寫法,總結(jié)在一起,更能融會貫通
直接使用字面量(全局對象)
const person = {
name: '哈哈',
age: 18
}
了解 const 語法的小伙伴都知道,這只喵是不能被重新賦值的,但是它里面的屬性其實是可變的。
如果想要一個不可變的單例對象:
const person = {
name: '哈哈',
age: 18
}
Object.freeze(person);
這樣就不能新增或修改person的任何屬性.
如果是在模塊中使用,上面的寫法并不會污染全局作用域,但是直接生成一個固定的對象缺少了一些靈活性。
使用構(gòu)造函數(shù)的靜態(tài)屬性
class寫法
class A {
constructor () {
if (!A._singleton) {
A._singleton = this;
}
return A._singleton;
}
log (...args) {
console.log(...args);
}
}
var a1 = new A()
var a2= new A()
console.log(a1 === a2)//true
構(gòu)造函數(shù)寫法
function A(name){
// 如果已存在對應(yīng)的實例
if(typeof A._singleton === 'object'){
return A._singleton
}
//否則正常創(chuàng)建實例
this.name = name
// 緩存
A._singleton =this
return this
}
var a1 = new A()
var a2= new A()
console.log(a1 === a2)//true
缺點:在于靜態(tài)屬性是能夠被人為重寫的,不過不會像全局變量那樣被無意修改。
借助閉包
考慮重寫構(gòu)造函數(shù):當對象第一次被創(chuàng)建以后,重寫構(gòu)造函數(shù),在重寫后的構(gòu)造函數(shù)里面訪問私有變量。
function A(name){
var instance = this
this.name = name
//重寫構(gòu)造函數(shù)
A = function (){
return instance
}
//重寫構(gòu)造函數(shù)之后,實際上原先的A指針對應(yīng)的函數(shù)實際上還在內(nèi)存中(因為instance變量還在被引用著),但是此時A指針已經(jīng)指向了一個新的函數(shù)了
}
A.prototype.pro1 = "from protptype1"
var a1 = new A()
A.prototype.pro2 = "from protptype2"
var a2= new A()
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//underfined
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//underfined
console.log(a1.constructor ==== A) //false
為了解決A指針指向新地址的問題,實現(xiàn)原型鏈繼承
function A(name){
var instance = this
this.name = name
//重寫構(gòu)造函數(shù)
A = function (){
return instance
}
// 第一種寫法,這里實際上實現(xiàn)了一次原型鏈繼承,如果不想這樣實現(xiàn),也可以直接指向舊的原型
A.prototype = this
// 第二種寫法,直接指向舊的原型
A.prototype = this.constructor.prototype
instance = new A()
// 調(diào)整構(gòu)造函數(shù)指針,這里實際上實現(xiàn)了一次原型鏈繼承,如果不想這樣實現(xiàn),也可以直接指向原來的原型
instance.constructor = A
return instance
}
A.prototype.pro1 = "from protptype1"
var a1 = new A()
A.prototype.pro2 = "from protptype2"
var a2= new A()
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//from protptype2
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//from protptype2
利用立即執(zhí)行函數(shù)來保持私有變量
var A;
(function(name){
var instance;
A = function(name){
if(instance){
return instance
}
//賦值給私有變量
instance = this
//自身屬性
this.name = name
}
}());
A.prototype.pro1 = "from protptype1"
var a1 = new A('a1')
A.prototype.pro2 = "from protptype2"
var a2 = new A('a2')
console.log(a1.name)
console.log(a1.pro1)//from protptype1
console.log(a1.pro2)//from protptype2
console.log(a2.pro1)//from protptype1
console.log(a2.pro2)//from protptype2
以上通過閉包的方式可以實現(xiàn)單例
代理實現(xiàn)單例模式
function singleton(name){
this.name = name
}
let proxySingleton = function(){
let instance = null
return function(name){
if(!instance){
instance = new singleton(name)
}
return instance
}
}()
let a1= new proxySingleton('a1')
let a2= new proxySingleton('a2')
console.log(123, a1===a2)
工廠單例
let logger = null
class Logger {
log (...args) {
console.log(...args);
}
}
function createLogger() {
if (!logger) {
logger = new Logger();
}
return logger;
}
let a = new createLogger().log('12')
let b = new createLogger().log('121')
console.log(new createLogger(), a===b)
根據(jù)理解,我自己喜歡用代理方式實現(xiàn),更好理解。如果總結(jié)有錯,歡迎指正。
參考:單例模式
工廠模式
不暴露創(chuàng)建對象的邏輯,封裝在一個函數(shù)中。工廠模式根據(jù)抽象程度的不同可以分為:簡單工廠,工廠方法和抽象工廠。
簡單工廠模式
簡單工廠模式又叫靜態(tài)工廠模式,由一個工廠對象決定創(chuàng)建某一種產(chǎn)品對象類的實例。主要用來創(chuàng)建同一類對象。
簡單工廠的優(yōu)點在于,你只需要一個正確的參數(shù),就可以獲取到你所需要的對象,而無需知道其創(chuàng)建的具體細節(jié)。
但是在函數(shù)內(nèi)包含了所有對象的創(chuàng)建邏輯(構(gòu)造函數(shù))和判斷邏輯的代碼,每增加新的構(gòu)造函數(shù)還需要修改判斷邏輯代碼。當我們的對象不是上面的3個而是30個或更多時,這個函數(shù)會成為一個龐大的超級函數(shù),便得難以維護。所以,簡單工廠只能作用于創(chuàng)建的對象數(shù)量較少,對象的創(chuàng)建邏輯不復雜時使用。
工廠方法模式
工廠方法模式的本意是將實際創(chuàng)建對象的工作推遲到子類中,這樣核心類就變成了抽象類。
在工廠方法模式中,工廠父類負責定義創(chuàng)建產(chǎn)品對象的公共接口,而工廠子類則負責生成具體的產(chǎn)品對象, 這樣做的目的是將產(chǎn)品類的實例化操作延遲到工廠子類中完成,即通過工廠子類來確定究竟應(yīng)該實例化哪一個具體產(chǎn)品類。
抽象工廠模式
抽象工廠其實是實現(xiàn)子類繼承父類的方法。
抽象工廠模式(Abstract Factory Pattern),提供一個創(chuàng)建一系列相關(guān)或相互依賴對象的接口,而無須指定它們具體的類。
抽象工廠可以提供多個產(chǎn)品對象,而不是單一的產(chǎn)品對象。
參考 :JavaScript設(shè)計模式與實踐--工廠模式
觀察者模式或發(fā)布訂閱模式
通常又被稱為 發(fā)布-訂閱者模式 或 消息機制,它**定義了對象間的一種一對多的依賴關(guān)系**,只要當一個對象的狀態(tài)發(fā)生改變時,所有依賴于它的對象都得到通知并被自動更新,解決了主體對象與觀察者之間功能的耦合,即一個對象狀態(tài)改變給其他對象通知的問題。
最好理解的舉例:公司里發(fā)布通知,讓員工都知道。
工作中碰到以下幾種,并進行分析。
用雙向綁定來分析此模式:
雙向綁定維護4個模塊:observer監(jiān)聽者、dep訂閱器、watcher訂閱者、compile編譯者
訂閱器是手機訂閱者(依賴),如果屬性發(fā)生變化observer通知dep,dep通知watcher調(diào)用update函數(shù)(watcher類中有update函數(shù),并且將自己加入dep)去更新數(shù)據(jù),這是符合一對多的思想,也就是observer是一,watcher是多。compile解析指令,訂閱數(shù)據(jù)變化,綁定更新函數(shù)。
理解下來,compile類似于綁定員工的角色,把watcher加入一個集體,observer通知它們執(zhí)行。
用子組件與父組件通信分析此模式:
通過 emit來發(fā)布消息,對訂閱者emit 來發(fā)布消息,對訂閱者 emit來發(fā)布消息,對訂閱者on 做統(tǒng)一處理。
emit是發(fā)布訂閱者,emit是發(fā)布訂閱者,emit是發(fā)布訂閱者,on 是監(jiān)聽執(zhí)行
用DOM的事件綁定(比如click)分析此模式:
addEventListener('click',()=>{})監(jiān)聽click事件,當點擊DOM就是向訂閱者發(fā)布這個消息。
點擊DOM是發(fā)布,addEventListener是監(jiān)聽執(zhí)行
小結(jié)
通過分析平時碰到的這種模式,更好的理解一和多分別對應(yīng)什么,也增加記憶。
一(發(fā)布事件):通知、廣播發(fā)布
多(訂閱事件,可能會做出不同的回應(yīng)):觀察者、監(jiān)聽者、訂閱者
發(fā)布和訂閱的意思都是變成多的角度(添加)
一是對應(yīng)執(zhí)行,多是收集
平時碰到的函數(shù)理解,寫自己的函數(shù)也可以這么定義,有個全局Events=[]:
publish/emit/點擊/notify 訂閱事件 調(diào)用函數(shù) subscribe/on/addEventListener 監(jiān)聽 push函數(shù)
unsubscribe/off/removeEventListener 刪除
其實分析以上幾種情況,發(fā)布訂閱模式和觀察者模式的思想差不多相同,但是也是有區(qū)別:
觀察者模式中需要觀察者對象自己定義事件發(fā)生時的相應(yīng)方法。 發(fā)布訂閱模式者在發(fā)布對象和訂閱對象之中加了一個中介對象。我們不需要在乎發(fā)布者對象和訂閱者對象的內(nèi)部是什么,具體響應(yīng)時間細節(jié)全部由中介對象實現(xiàn)。
訂閱的東西用Map或者Object類型來存儲。
發(fā)布訂閱模式,有個中介,也可以說是channel,但發(fā)現(xiàn)代碼實現(xiàn)差不多,只不過發(fā)布訂閱用來寫包含有回調(diào)函數(shù)
實例參考:JavaScript 設(shè)計模式之觀察者模式與發(fā)布訂閱模式
juejin.cn/post/684490…
juejin.cn/post/684490…
裝飾者模式
裝飾者模式的定義:在不改變對象自身的基礎(chǔ)上,在程序運行期間給對象動態(tài)地添加方法。
裝飾者模式的適用場合:
如果你需要為類增添特性或職責,可是從類派生子類的解決方法并不太現(xiàn)實的情況下,就應(yīng)該使用裝飾者模式。 如果想為對象增添特性又不想改變使用該對象的代碼的話,則可以采用裝飾者模式。 原有方法維持不變,在原有方法上再掛載其他方法來滿足現(xiàn)有需求;函數(shù)的解耦,將函數(shù)拆分成多個可復用的函數(shù),再將拆分出來的函數(shù)掛載到某個函數(shù)上,實現(xiàn)相同的效果但增強了復用性。
裝飾者模式除了可以應(yīng)用在類上之外,還可以應(yīng)用在函數(shù)上(其實這就是高階函數(shù))
我覺得可以是函數(shù)封裝原函數(shù)。這樣不改變原來
舉例:為汽車添加反光燈、后視鏡等這些配件
碰到的:對函數(shù)進行增強(節(jié)流函數(shù)or防抖函數(shù)、緩存函數(shù)返回值、構(gòu)造React高階組件,為組件增加額外的功能)
參考: 使用裝飾者模式做有趣的事情
代理模式
所謂的的代理模式就是為一個對象找一個替代對象,以便對原對象進行訪問。
使用代理的原因是我們不愿意或者不想對原對象進行直接操作,我們使用代理就是讓它幫原對象進行一系列的操作,等這些東西做完后告訴原對象就行了。就像我們生活的那些明星的助理經(jīng)紀人一樣。
原則:單一原則
常用的虛代理形式:保護代理、緩存代理、虛擬代理。
保護代理:明星委托助理或者經(jīng)紀人所要干的事;
緩存代理:緩存代理就是將代理加緩存,更方便單一原則;
常用的虛擬代理:某一個花銷很大的操作,可以通過虛擬代理的方式延遲到這種需要它的時候才去創(chuàng)建(例:使用虛擬代理實現(xiàn)圖片懶加載);
先占位,加載完,再加載所需圖片
var imgFunc = (function() {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();
var proxyImage = (function() {
var img = new Image();
img.onload = function() {
imgFunc.setSrc(this.src);
}
return {
setSrc: function(src) {
imgFunc.setSrc('./loading,gif');
img.src = src;
}
}
})();
proxyImage.setSrc('./pic.png');
碰到的:Vue的Proxy、懶加載圖片加占位符、冒泡點擊DOM元素
參考:javascript 代理模式(通俗易懂)
策略模式
策略模式的定義:定義一系列的算法,把他們一個個封裝起來,并且使他們可以相互替換。
策略模式的重心不是如何實現(xiàn)算法,而是如何組織、調(diào)用這些算法,從而讓程序結(jié)構(gòu)更靈活、可維護、可擴展。
策略模式的目的:將算法的使用算法的實現(xiàn)分離開來。
一個基于策略模式的程序至少由兩部分組成:
第一個部分是一組策略類(可變),策略類封裝了具體的算法,并負責具體的計算過程。 第二個部分是環(huán)境類Context(不變),Context接受客戶的請求,隨后將請求委托給某一個策略類。要做到這一點,說明Context中要維持對某個策略對象的引用。
原則:開放-封閉原則
/*策略類 A B C就是可以替換使用的算法*/
var levelOBJ = {
"A": function(money) {
return money * 4;
},
"B" : function(money) {
return money * 3;
},
"C" : function(money) {
return money * 2;
}
};
/*環(huán)境類,維持對levelOBJ策略對象的引用,擁有執(zhí)行算法的能力*/
var calculateBouns =function(level,money) {
return levelOBJ[level](money);
};
console.log(calculateBouns('A',10000)); // 40000
Context函數(shù)傳入實際值,調(diào)用策略,可能同時調(diào)用多個策略,這樣可以封裝一函數(shù)循環(huán)調(diào)用策略,然后用Context函數(shù)調(diào)用此封裝的函數(shù)
在工作中,很多if else,每種條件執(zhí)行不同的算法,其實可以用到策略模式,比如驗證表單
比如:
多種不同登錄方式(賬號密碼登錄、手機驗證碼登錄和第三方登錄)。為了方便維護不同的登錄方式,可以把不同的登錄方式封裝成不同的登錄策略。
驗證表單
不同的人發(fā)不同的工資
工作中碰到選擇不同下拉框執(zhí)行不同函數(shù)(策略)
參考:js設(shè)計模式--策略模式
建造者模式
應(yīng)用場景:
創(chuàng)建時有很多必填參數(shù)需要驗證。 創(chuàng)建時參數(shù)求值有先后順序、相互依賴。 創(chuàng)建有很多步驟,全部成功才能創(chuàng)建對象。
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()
適配模式
舉例:Target Adaptee Adapter ,Adapter是需要繼承Target,并在里面調(diào)用Adaptee中的方法。
形象比擬:Target是目標抽象類,實現(xiàn)插入插口的功能;Adaptee是新的插頭,包含了實現(xiàn)目標的方法;Adapter是implements Target,為了調(diào)用Target方法。這樣,既能保留原功能(原函數(shù)不變),又能執(zhí)行新功能(添加Adaptee Adapter)
Target是要實現(xiàn)的目標(比如打印日志,這是抽象的方法),如果不用適配模式,就需要重寫函數(shù),找到辦法。
Adaptee是適配者類,也就是插口,在適配器Adapter中,implements來自于的Target方法(就是新方法,適配的方法,達到)中調(diào)用Adaptee中的方法。
Adaptee在Adapter中調(diào)用,Adapter最終是要調(diào)用Adaptee 中需要的具體方法(也就是我們最終要達到目的使用的方法,此方法可在Target中的抽象方法中實現(xiàn))。
場景:
1.以前開發(fā)的接口不滿足需求,比如輸出log存在本地盤改成存入云盤
2.使用第三方提供的組件,但組件接口定義和自己要求的接口定義不同
面試過程中,定義Target Adapter Adaptee分別實現(xiàn)的功能,再套用原理來實現(xiàn)。
用ts實現(xiàn),這樣能使用interface和implements,更符合面向?qū)ο笳Z言
優(yōu)點
將目標類和適配者類解耦,通過引入一個適配器類來重用現(xiàn)有的適配者類,而無須修改原有代碼。
增加了類的透明性和復用性,將具體的實現(xiàn)封裝在適配者類中,對于客戶端類來說是透明的,而且提高了適配者的復用性。
靈活性和擴展性都非常好,通過使用配置文件,可以很方便地更換適配器,也可以
在不修改原有代碼的基礎(chǔ)上增加新的適配器類,符合開閉原則。
缺點
過多地使用適配器,會讓系統(tǒng)非常零亂,不易整體進行把握。
模板方法模式
很好理解,看它能很快理解
這九種常用的設(shè)計模式你掌握了嗎
職責鏈模式
應(yīng)用場景:
多個處理器 ABC 依次處理同一個請求,形成一個鏈條,當某個處理器能處理這個請求,就不會繼續(xù)傳遞給后續(xù)處理器了。 過濾器 攔截器 處理器。
const order500 = function (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log("500 元定金預購, 得到 100 元優(yōu)惠券");
return true;
} else {
return false;
}
};
const order200 = function (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log("200 元定金預購, 得到 50 元優(yōu)惠券");
return true;
} else {
return false;
}
};
const orderCommon = function (orderType, pay, stock) {
if ((orderType === 3 || !pay) && stock > 0) {
console.log("普通購買, 無優(yōu)惠券");
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); // 普通購買, 無優(yōu)惠券
鏈式
其他參考
關(guān)于本文
作者:hannie76327
https://juejin.cn/post/6953872475537014820
