摘要:代理模式,迭代器模式,單例模式,裝飾者模式最少知識(shí)原則一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少地與其他實(shí)體發(fā)生相互作用。迭代器模式可以將迭代的過程從業(yè)務(wù)邏輯中分離出來,在使用迭代器模式之后,即不用關(guān)心對(duì)象內(nèi)部構(gòu)造也可以按順序訪問其中的每個(gè)元素。
接手項(xiàng)目越來越復(fù)雜的時(shí)候,有時(shí)寫完一段代碼,總感覺代碼還有優(yōu)化的空間,卻不知道從何處去下手。設(shè)計(jì)模式主要目的是提升代碼可擴(kuò)展性以及可閱讀性。
本文主要以例子的方式展示設(shè)計(jì)模式應(yīng)該如何使用!(例子主要來源于javascript設(shè)計(jì)模式一書,如果已經(jīng)對(duì)這本書讀得滾瓜爛熟的,可以劃過,如果還未讀,或者想了解一下可以收藏起來慢慢看~)
在使用設(shè)計(jì)模式前應(yīng)該需要知道的幾個(gè)原則(其中對(duì)應(yīng)設(shè)計(jì)模式滿足對(duì)應(yīng)原則):
單一職責(zé)原則(SRP): 一個(gè)對(duì)象(只做一件事)。
代理模式,迭代器模式,單例模式,裝飾者模式
最少知識(shí)原則(LKP): 一個(gè)軟件實(shí)體應(yīng)當(dāng)盡可能少地與其他實(shí)體發(fā)生相互作用。
中介者模式
開放-封閉原則(OCP):軟件實(shí)體(類,模塊,函數(shù))應(yīng)該都是可以擴(kuò)展,但是不可修改
發(fā)布-訂閱模式,模板方法模式,策略模式,代理模式,職責(zé)鏈模式
代理模式代理顧名思義,就是客服無法直接與本體進(jìn)行的溝通通過第三方進(jìn)行轉(zhuǎn)述。虛擬代理
作為創(chuàng)建開銷大的對(duì)象的代表;虛擬代理經(jīng)常直到我們真正需要一個(gè)對(duì)象的時(shí)候才創(chuàng)建它;當(dāng)對(duì)象在創(chuàng)建前或創(chuàng)建中時(shí),由虛擬代理來扮演對(duì)象的替身;對(duì)象創(chuàng)建后,代理就會(huì)將請(qǐng)求直接委托給對(duì)象;圖片預(yù)加載例子
const myImage = (function() { const imgNode = document.createElement("img"); document.body.appendChild(imgNode); return { setSrc: function(src) { imgNode.src = src; } } }()) // 代理容器 const proxyImage = (function() { let img = new Image(); // 加載完之后將設(shè)置為添加的圖片 img.onload = function() { myImage.setSrc(this.src) } return { setSrc: function(src) { myImage.setSrc("loading.gif"); img.src = src; } } }()) proxyImage.setSrc("file.jpg")
如上:代理容器控制了客戶對(duì)MyImage的訪問,并且在過程中加了一些額外的操作。
緩存代理緩存代理可以為一些開銷大的運(yùn)算結(jié)果提供暫時(shí)的存儲(chǔ),在下次運(yùn)算時(shí),如果傳遞進(jìn)來的參數(shù)跟之前一致,則可以直接返回前面存儲(chǔ)的運(yùn)算結(jié)果。計(jì)算乘積例子
// 求乘積函數(shù)(專注于自身職責(zé),計(jì)算成績(jī),緩存由代理實(shí)現(xiàn)) const mult = function() { let a = 1; for (let i = 0, l =arguments.length; i< l; i++){ a = a * arguments[i]; } return a; } // proxyMult const proxyMult = (function() { let cache = {}; return function() { let args = Array.prototype.join.call(arguments, ","); if (args in cache) { return cache[args]; } return cache[arg] = mult.apply(this, arguments); } }()) proxyMult(1, 2, 3) // 6 proxyMult(1, 2, 3) // 6迭代器模式
迭代器模式是指提供一種方法順序訪問一個(gè)聚合對(duì)象中的各個(gè)元素,而又不需要暴露該對(duì)象的內(nèi)部表達(dá)式。迭代器模式可以將迭代的過程從業(yè)務(wù)邏輯中分離出來,在使用迭代器模式之后,即不用關(guān)心對(duì)象內(nèi)部構(gòu)造也可以按順序訪問其中的每個(gè)元素。
迭代器分為內(nèi)部迭代器和外部迭代器,內(nèi)部迭代器,是在函數(shù)內(nèi)部已經(jīng)定義好了迭代規(guī)則,外部只需要調(diào)用即可。但如果要修改需求,那就要去迭代函數(shù)內(nèi)部去修改邏輯了。外部迭代器指的是必須顯示的請(qǐng)求迭代下一個(gè)元素。
如現(xiàn)在有一個(gè)需求,判斷兩個(gè)函數(shù)是否完全相等,分別使用內(nèi)部迭代器和外部迭代器去實(shí)現(xiàn)。
// 使用內(nèi)部迭代的方式實(shí)現(xiàn) compare const compare = function (arr1, arr2) { try { if (arr1.length !== arr2.length) { throw "arr1和arr2不相等" } // forEach 相當(dāng)一于一個(gè)迭代器 arr1.forEach((item, index) => { if (item !== arr2[index]) { throw "arr1和arr2不相等" } }) console.log("arr1等于arr2") } catch (e) { console.log(e) } }
使用外部迭代器模式改寫compare
// 迭代器 const iterator = function (obj) { let current = 0; let next = function () { current += 1; }; let isDone = function () { return current >= obj.length; } let getCurrItem = function () { return obj[current]; } return { next: next, isDone: isDone, getCurrItem: getCurrItem, length: obj.length } } // 重寫compare const compare = function (iterator1, iterator2) { try { if (iterator1.length !== iterator2.length) { throw "iterator1不等于iterator2" } while (!iterator1.isDone() && !iterator2.isDone()) { if (iterator1.getCurrItem() !== iterator2.getCurrItem()) { throw "iterator1不等于iterator2" } iterator1.next(); iterator2.next(); } console.log("iterator1 === iterator2") } catch (e) { console.log(e) } } const iterator1 = iterator([1, 2, 3]); const iterator2 = iterator([1, 2, 3]); compare(iterator1, iterator2)
迭代器實(shí)際場(chǎng)景的應(yīng)用
根據(jù)不同的瀏覽器獲取相應(yīng)上傳組件對(duì)象
// 常規(guī)寫法 const getUploadObj = function() { try { return new ActiveXObject("txftna") } catch (e) { if (supportFlash()) { let str = `` return document.body.appendChild(str) } else { let str = ``; return document.body.appendChild(str); } } } // 迭代模式改寫 // IE上傳控件 const getActiveUploadObj = function () { try { return new ActiveXObject("TXFTNActiveX.FTNUPload") } catch (e) { return false; } } // flash上傳控件 const getFlashUploadObj = function () { if (supportFlash()) { let str = `` return document.body.appendChild(str) } return false } // 表單上傳 const getFormUploadObj = function () { let str = ``; return document.body.appendChild(str); } // 使用迭器執(zhí)行 const iteratorUploadObj = function () { for (let i = 0, fn; fn = arguments[i++];) { const uploadInstane = fn() if (uploadInstane !== false) { return uploadInstane } } } iteratorUploadObj(getActiveUploadObj, getFlashUploadObj, getFormUploadObj)
觀察上面經(jīng)過未使用迭代器模式,和使用迭代器模式后,發(fā)現(xiàn)不使用的話,如果后期想新增一個(gè)其他的模式,需要修改原來代碼的邏輯,如果使用迭代器的模式的話只需要新增一個(gè)方法即可。雖然在寫的時(shí)候代碼多了幾行,但總的來說,后期擴(kuò)展以及可閱讀性明顯提高了~。
單例模式定義:單例模式指的是保證一個(gè)類僅有一個(gè)實(shí)例,且提供一個(gè)訪問它的全局訪問點(diǎn)。全局緩存,window對(duì)象,都可以看作是一個(gè)單例。
目的: 解決一個(gè)全局使用的類頻繁地創(chuàng)建與銷毀
使用ES6 class 創(chuàng)建單例:class Instance { static init () { if (!this.instance) { this.instance = new Instance() } return this.instance; } } const instance1 = Instance.init() const instance2 = Instance.init() console.log(instance1 === instance2) //true使用閉包創(chuàng)建單例:
const instance = (function() { let instance = null; return function(name) { if (!instance) { instance = new Singleton(name) } return instance; } }())使用代理實(shí)現(xiàn)單例模式
以在頁(yè)面上創(chuàng)建唯一的dom節(jié)點(diǎn)為例;
// 創(chuàng)建div類 class CreateDiv { constructor(html) { this.html = html this.init() } init() { let div = document.createElement("div"); div.innerHTML = this.html; document.body.appendChild(div) } } // 代理類 class ProxySingletonCreateDiv { static initInstance(html) { if (!this.instance) { this.instance = new CreateDiv(html) } return this.instance; } } ProxySingletonCreateDiv.initInstance("test1"); ProxySingletonCreateDiv.initInstance("test2");
測(cè)試上面這段代碼會(huì)發(fā)現(xiàn)頁(yè)面上只會(huì)顯示test1,因?yàn)閷?shí)例只創(chuàng)建了一次,也就是new CreateDiv只執(zhí)行了第一次,第二次并沒有執(zhí)行。
惰性單例惰性單例是指在需要的時(shí)候才創(chuàng)建對(duì)象實(shí)例。(在一定場(chǎng)景下,用戶只有在需要的時(shí)候才創(chuàng)建)
例:instance實(shí)例總是在我們調(diào)用getInstance的時(shí)候才會(huì)被創(chuàng)建
Singleton.getInstance = (function () { let instance = null; return function (name) { if (!instance) { instance = new Singleton(name) } return instance; } }()) const instance = Singleton.getInstance("hello")
實(shí)現(xiàn)通用惰性單例
const getSingle = function (fn) { let result; return function() { return result || (result = fn.apply(this, arguments)) } }裝飾者模式
裝飾者模式指的是:可以動(dòng)態(tài)地給某個(gè)對(duì)象添加一些額外的職責(zé),而不會(huì)影響從這個(gè)類中派生的其他對(duì)象。最基礎(chǔ)的裝飾者
以編寫一個(gè)飛機(jī)大戰(zhàn)的游戲?yàn)槔S著等級(jí)的增加最開始我們只能發(fā)送普通的子彈,二級(jí)可以發(fā)送導(dǎo)彈,三級(jí)可以發(fā)送原子彈。
// 飛機(jī)對(duì)象 let plane = { fire: function () { console.log("發(fā)射普通子彈") } }; // 發(fā)送導(dǎo)彈的方法,實(shí)際內(nèi)容略 let missileDecorator = function () { console.log("發(fā)射導(dǎo)彈"); }; // 發(fā)送原子彈的方法 let atomDecorator = function () { console.log("發(fā)射原子彈"); }; // 將發(fā)送子彈方法存起來, let fire1 = plane.fire; // 裝飾發(fā)送普通子彈的方法 plane.fire = function () { fire1(); missileDecorator() }; let fire2 = plane.fire; plane.fire = function () { fire2(); atomDecorator() }; plane.fire() // 發(fā)射普通子彈,發(fā)射導(dǎo)彈,發(fā)射原子彈
我們有時(shí)候在維護(hù)代碼的時(shí)候,可能會(huì)遇到這樣的需求。比如給window綁定onload事件,但又不太確定這個(gè)事件是否被其他人綁定過了,為了避免覆蓋掉之前的window.onload的函數(shù)的行為,我們一般會(huì)向上面的例子一樣,將之前的行為保存在一個(gè)變量?jī)?nèi),然后再給綁定的window.onload函數(shù)添加這個(gè)變量執(zhí)行從而滿足需求。
以上方法的缺陷:
需要多維護(hù)了中間變量,如上面的例子fire1和fire2,如果鏈越來越長(zhǎng),那么維護(hù)的就越來越多。
還會(huì)遇到this劫持問題,如上fire函數(shù)被變量存起來的時(shí)候plane.fire執(zhí)行時(shí)this指向global(node環(huán)境)
AOP裝飾函數(shù)為解決上面this的劫持問題,延伸實(shí)現(xiàn)Function.prototype.before和Function.prototype.after方法:
Function.prototype.before = function (beforeFn) { let that = this; // 保存原函數(shù)的引用 return function () { beforeFn.apply(this, arguments); return that.apply(this, arguments); //執(zhí)行原函數(shù),且保證this不被劫持 } }; Function.prototype.after = function (afterFn) { let that = this; return function () { let ret = that.apply(this, arguments); afterFn.apply(this, arguments); return ret; } };
改寫上面飛機(jī)的例子:
let plane = { fire: function () { console.log("發(fā)射普通子彈!") } }; plane.fire.after(function () { console.log("發(fā)射導(dǎo)彈!") }).after(function () { console.log("發(fā)射原子彈!") })()
很明顯的看見解決了上面的兩個(gè)缺陷。
AOP應(yīng)用實(shí)例之?dāng)?shù)據(jù)上報(bào)做前端開發(fā),主要提升用戶體驗(yàn),所以有時(shí)候在項(xiàng)目結(jié)尾為了能更多的收集到用戶的操作數(shù)據(jù)不得不加入一些埋點(diǎn)數(shù)據(jù)在業(yè)務(wù)中。如:點(diǎn)擊上報(bào)多少人點(diǎn)擊登錄按鈕來顯示登錄浮窗。
// 常規(guī)做法 bad let log = function () { console.log("上報(bào)")// 實(shí)際內(nèi)容略 } let showLogin = function () { console.log("打開登錄浮窗"); log(); } // 上面做法,showLogin既要做顯示彈窗的操作,又要負(fù)責(zé)數(shù)據(jù)上報(bào),違反了單一職責(zé)原則 // 使用裝飾者方式改寫 good let showLogin1 = function () { console.log("顯示彈窗") } let showLogin = showLogin.after(log); document.getElement("button").onclick = showLogin;
裝飾者模式與代碼模式的區(qū)別:
代理模式強(qiáng)調(diào)的是代理與它的實(shí)體之間的關(guān)系(這種關(guān)系在一開始就可以被確定),裝飾者模式用于一開始無法確定對(duì)象的全部功能場(chǎng)景。代理模式通常只有一層代理。而裝飾者會(huì)形成一條長(zhǎng)長(zhǎng)的裝飾鏈。
中介者指的是解除對(duì)象與對(duì)象之間的緊耦關(guān)系,增加中介者之后,所有對(duì)象通過中介者來通信,而不是互相引用,所以當(dāng)一個(gè)對(duì)象發(fā)生改變時(shí),只需要通知中介者對(duì)象即可。
現(xiàn)實(shí)生活中很的中介者場(chǎng)景,如:快遞物流公司,快遞員將負(fù)責(zé)的區(qū)域拿到用戶的快遞之后將快遞送到中轉(zhuǎn)場(chǎng),然后中轉(zhuǎn)場(chǎng)再進(jìn)行整理之后輸出分好類的區(qū)域,再由快遞員送到指定區(qū)域。在設(shè)計(jì)模式中,中轉(zhuǎn)場(chǎng)就扮演者,中介者的角色。
如圖將 A,B,C,D互相關(guān)聯(lián)的東西使用一個(gè)中介者進(jìn)行管理,減少A,B,C,D內(nèi)的互相引用。
const createAgent = (function () { return { add() => { //添加一個(gè)東西 代碼略 }, send () => { // 發(fā)送一個(gè)東西 代碼略 } } }()) const createA = function () { createAgent.add() setTimeout(() => { createAgent.send(); // 代碼略 }, 3000) } const createB = function () { //同上面方法類似 }
代碼只是說明中介者的意圖,內(nèi)容不要在意。
當(dāng)關(guān)聯(lián)的東西越來越多的時(shí)候中介者模式會(huì)變得越來越大,雖然會(huì)帶來這個(gè)缺點(diǎn),但取舍一下還是會(huì)比相互之間引用會(huì)更好。
發(fā)布-訂閱模式發(fā)布-訂閱模式:定義對(duì)象之間的一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都將得到通知!
常見的javascript事件指令,也是一種發(fā)布訂閱模式,
document.body.addEventListener("click", function () { // 一堆操作 }, false) // 模擬用戶點(diǎn)擊 document.body.click()
這里監(jiān)控用戶點(diǎn)擊document.body的動(dòng)作,但我們并不知道用戶什么時(shí)候會(huì)點(diǎn)擊,所以我們訂閱document.body上的click事件,當(dāng)body被點(diǎn)擊后,body節(jié)點(diǎn)會(huì)向訂閱者發(fā)布這個(gè)消息。
發(fā)布與訂閱模式的實(shí)現(xiàn)步驟:
首先要指定誰(shuí)充當(dāng)發(fā)布者。
然后給發(fā)布者添加一個(gè)緩存列表,用于存放回調(diào)函數(shù)以便通知訂閱者
最后發(fā)布消息的時(shí)候,發(fā)布者會(huì)便利緩存列表依次觸發(fā)里面存放的訂閱者回調(diào)函數(shù)
以售樓處去購(gòu)買房子為例:A去售樓部查看了一下房源,并告知了銷售者小姐姐自己的電話以及自己需要的房源類型(這里A充當(dāng)訂閱者),售樓處將用戶的電話以及需要房源類型記錄在小本子上(這個(gè)小本子相當(dāng)于緩存列表),售樓處充當(dāng)發(fā)布者!下面以一段代碼實(shí)現(xiàn)這段描述:
// 定義售樓處 let saleOffices = { // 存放訂閱者的回調(diào)函數(shù),即用戶的電話以及需求 clientList: {}, // 訂閱消息 listen: function (key, fn) { // 如果沒有訂閱過此類消息,則創(chuàng)建一個(gè)緩存列表 if (!this.clientList[key]) { this.clientList[key] = []; } // 將訂閱的消息添加進(jìn)消息緩存列表 this.clientList[key].push(fn); }, // 發(fā)布消息 trigger: function () { let key = Array.prototype.shift.call(arguments ); let fns = this.clientList[key]; // 如果沒訂閱此消息則返回 if (!fns || fns.length === 0) return false; for (let i = 0, fn; fn = fns[i++];) { // arguments是發(fā)布消息的附送參數(shù) fn.apply(this, arguments); } }, // 取消訂閱 remove: function (key, fn) { let fns = this.clientList[key]; if (!fns) return false; // 如果沒有傳入回調(diào)函數(shù),則取消key所有的訂閱 if (!fn) { fns && (fns.length = 0) } else { for (let l = fns.length - 1; l >= 0; l--) { let _fn = fns[l]; if (_fn === fn) { // 刪除訂閱的回調(diào)函數(shù) fns.splice(l, 1) } } } } }; let fn1 = function (price) { console.log("價(jià)格=", price) } saleOffices.listen("listen100", fn1) saleOffices.remove("listen100", fn1) saleOffices.trigger("listen100", 20000)
看了發(fā)布與訂閱模式和中介者模式,發(fā)現(xiàn)兩者之間有著很多相似之處。
發(fā)布-訂閱與中介者之間的區(qū)別:
中介者目的是為了減少對(duì)象之間的耦合,而且類里的內(nèi)容可能存在著不同對(duì)象之間需要的一些東西存儲(chǔ),后期可能是某個(gè)對(duì)象自己去取。發(fā)布-訂閱主要也是解決對(duì)象之間的耦合,不同的是發(fā)布訂閱是取決用戶關(guān)注什么東西后發(fā)布者在有了這個(gè)東西之后主動(dòng)推送給訂閱者~模板方法模式
模板模式指的是一種只需要使用繼承就可以實(shí)現(xiàn)的非常簡(jiǎn)單的模式 ,比較依賴于抽象類的一種設(shè)計(jì)模式,主要由抽象父類和具體實(shí)現(xiàn)子類組成!
抽象類可以表示一種契約,繼承了這個(gè)抽象類的所有子類都將擁有跟抽象類一致的接口方法,抽象類的主要作用就是為了它子類定義這些公共接口!
咖啡與茶的例子泡茶與沖咖啡:
首先可以將茶和咖啡抽象成飲料。
兩個(gè)在泡和沖都有相似的步驟:
把水煮沸
用沸水沖泡飲料
把飲料倒進(jìn)杯子
加調(diào)料
在類的繼承情況下,有時(shí)會(huì)存在子類未實(shí)現(xiàn)父類里已經(jīng)調(diào)用過的一些方法,常見解決可以在父類里添加對(duì)象的方法并提示一個(gè)錯(cuò)誤如:
Beverage.prototype.brew = function () { throw new Error("子類必須重寫brew方法"); }
實(shí)現(xiàn)沖咖啡與泡茶的例子:
// 抽象類 class Beverage { boilWater () { console.log("把水煮沸"); } brew () { throw new Error("子類必須實(shí)現(xiàn)此方法!") } pourInCup () { throw new Error("子類必須實(shí)現(xiàn)此方法!") } addCondiments () { throw new Error("子類必須實(shí)現(xiàn)此方法!") } // 構(gòu)子方法是否需要添加調(diào)料 customerWantsCondiments() { return true } init () { this.boilWater(); this.brew(); this.pourInCup(); if (this.customerWantsCondiments()) { this.addCondiments(); } } } // 咖啡類 class Coffee extends Beverage{ constructor(props) { super(props) } brew() { console.log("用沸水煮咖啡") } pourInCup() { console.log("把咖啡倒進(jìn)杯子") } addCondiments() { console.log("加糖和牛奶") } customerWantsCondiments () { return false; } } // 泡茶類 class Tea extends Beverage { constructor(props) { super(props); } brew () { console.log("用沸水浸泡茶葉") } pourInCup () { console.log("將茶水倒進(jìn)杯子") } addCondiments () { console.log("加對(duì)應(yīng)配料") } } let coffee = new Coffee() coffee.init() // 把水煮沸 用沸水煮咖啡 把咖啡倒進(jìn)杯子 let tea = new Tea() tea.init(); // 把水煮沸 用沸水浸泡茶葉 將茶水倒進(jìn)杯子 加對(duì)應(yīng)配料好萊塢原則
許多新人演員在好萊塢把簡(jiǎn)歷遞給演藝公司之后就只有回家等待盡管。有時(shí)候演員等得不耐煩了,給演藝公司打電話詢問情況,演藝公司往往這樣回答:“不要來差我,我會(huì)給你打電話。”,這就是好萊塢原則。
好萊塢原則,允許底層組件將自己掛鉤到高層組件中,而高層組件會(huì)決定什么時(shí)候,什么方式使用這些底層組件,與好萊塢原則一樣。
在javascript中,我們很多時(shí)候不需要用類這樣繁瑣的方式實(shí)現(xiàn)模板方法,使用高階函數(shù)更好。我們用高階函數(shù)改寫上面的例子。
const Beverage = function(param) { let boilWater = function() { console.log("把水煮沸") } let brew = param.brew || function() { throw new Error("必須傳遞brew方法") } let pourInCup = param.pourInCup || function() { throw new Error("必須傳遞pourInCup方法") } let addCondiments = param.addCondiments || function() { throw new Error("必須傳遞addCondiments") } let F = function () {}; F.prototype.init = function() { boilWater(); brew(); pourInCup(); addCondiments() } return F; } const Coffee = Beverage({ brew:function() { console.log("用沸水沖泡咖啡") }, pourInCup: function() { console.log("把咖啡倒進(jìn)杯子") }, addCondiments: function() { console.log("加糖和牛奶") } })策略模式
策略模式指的是:定義一系列算法,把它們一個(gè)個(gè)封裝起來,并且使它們可以互相替換。
策略模式優(yōu)點(diǎn):
策略模式例用組合、委托和多態(tài)等技術(shù)和思想,可以有效地避免多重條件選擇語(yǔ)句
策略模式提供了對(duì)開放-封閉原則的完美支持,將算法封裝在獨(dú)立的strategy中,使得它們易于切換,易于理解,易于擴(kuò)展。
策略模式中的算法也可以復(fù)用在系統(tǒng)的其他地方,從而避免許多重復(fù)的復(fù)制粘貼工作。
在策略模式中利用組合和委托來讓context擁有執(zhí)行算法的能力,這也是繼承的一種更輕便的替代方案。
表單校驗(yàn)例子校驗(yàn)邏輯:
用戶名不能為空,用戶名長(zhǎng)度不能小于10位
密碼長(zhǎng)度不能少于6位
手機(jī)號(hào)必須符合格式。
為了下面的javascript代碼更少的編寫html代碼,我這里將html代碼提取出來
// html 代碼
不使用策略模式我們正常的實(shí)現(xiàn)
const registerForm = document.getElementById("registerForm"); registerForm.onsubmit = function() { const userName = registerForm.userName.value if(userName === "" && userName.length >= 10) { console.log("用戶名不能為空") return false; } if (registerForm.password.value.length < 6) { console.log("密碼不能為空") return false; } if (!/(^1[3|5|8][0|9]{9}$)/.test(registerForm.phoneNumber.value)) { console.log("手機(jī)號(hào)輸入不正確") return false; } }
這樣的代碼會(huì)導(dǎo)致校驗(yàn)的函數(shù)越來越龐大,在系統(tǒng)變化的時(shí)候缺乏彈性。
使用策略模式重構(gòu)上面的表單校驗(yàn):
策略模式的組成部分:
策略類:封裝具體算法,并負(fù)責(zé)具體計(jì)算過程。
環(huán)境類:環(huán)境類Context,Context接受客戶的請(qǐng)求,隨后把請(qǐng)求委托給某一個(gè)策略類。
// 封裝算法 const strategies = { isNonEmpty: function (value, errorMsg) { if (value === "") { return errorMsg; } }, minLength: function (value, length, errorMsg) { if (value.length < length) { return errorMsg; } }, isMobile: function (value, errorMsg) { if (!/(^1[3|5|8][0|9]{9}$)/.test(value)) { return errorMsg; } } } // 實(shí)現(xiàn)Context環(huán)境類 const Validator = function () { this.cache = []; } Validator.prototype = { add (dom, rules) { let self = this; for (let i = 0, rule; rule = rules[i++];) { let strategyAry = rule.strategy.split(":"); let errorMsg = rule.errorMsg; self.cache.push(function () { 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: "isNonEmpty", errorMsg: "用戶名不能為空" }, { strategy: "minLength:10", errorMsg: "用戶名長(zhǎng)度不能小于10位" }]) validator.add(registerForm.password, [{ strategy: "minLength:6", errorMsg: "密碼長(zhǎng)度不能小于6位" }]) validator.add(registerForm.phoneNumber, [{ strategy: "isMobile", errorMsg: "手機(jī)號(hào)碼格式不正確" }]) let errorMsg = validator.start(); return errorMsg; } registerForm.onsubmit = function () { let errorMsg = validataFunc(); if (errorMsg) { console.log("errorMsg"); return false; } }
雖然看起來代碼多了很多,但對(duì)于以后的維護(hù)和擴(kuò)展方法,復(fù)用方法,這種方式明顯會(huì)好很多。
職責(zé)鏈模式職責(zé)鏈模式指的是:使多個(gè)對(duì)象都有機(jī)會(huì)處理請(qǐng)求,從而避免請(qǐng)求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對(duì)象連成一個(gè)鏈條,并沿著鏈條傳遞這些請(qǐng)求,直到有一個(gè)對(duì)象處理它為止。
職責(zé)鏈生活例子:
中學(xué)時(shí)期末考試,如果你平時(shí)不老實(shí)考試時(shí)就會(huì)被安排到第一排的位置,遇到不會(huì)答的題就會(huì)把題寫在紙條上傳給后排的同學(xué),如果后排同樣不會(huì)繼續(xù)往后排傳值到會(huì)為止。
實(shí)際開發(fā)中職責(zé)鏈模式使用場(chǎng)景:
需求是這樣的:
商場(chǎng)在預(yù)定手機(jī)時(shí),分別繳納500和200的定金,到的購(gòu)買階段后交付500定金的可以收到100優(yōu)惠券,200的可以收到50優(yōu)惠券,沒有支付定金沒有優(yōu)惠券且只能保證購(gòu)買時(shí)庫(kù)存有貨時(shí)能購(gòu)買成功!
平常我們拿到這樣的需求后可能會(huì)寫下面這樣的代碼:
const fn = function(stock) { if (stock > 0) { console.log("普通購(gòu)買,無優(yōu)惠券") } else { console.log("手機(jī)庫(kù)存不足") } } /** * @param {number} orderType 訂單類型1,2,3 * @param {boolean} pay 是否支付定金 true | false * @param {number} stock 庫(kù)存數(shù)量 */ let order = function(orderType, pay, stock) { if (orderType === 1) { if (pay === true) { console.log("500元定金預(yù)購(gòu),得到100優(yōu)惠券。") } else { fn(stock); } } else if (olderType === 2) { if (pay === true) { console.log("200元,50優(yōu)惠券") } else { fn(stock) } } else if (orderType === 3) { fn(stock) } }
看上面的代碼,邏輯上也沒什么問題,但相信我們?cè)趯懙臅r(shí)候一般也不會(huì)這樣去寫,因?yàn)檫@樣在后期維護(hù)的時(shí)候order函數(shù)會(huì)變得越來越龐大,而且要新增一些其他的邏輯也是比較困難。
使用職責(zé)鏈模式重寫上面的例子:
拆分條件語(yǔ)句,將每個(gè)條件提取成一個(gè)函數(shù)。
約定一個(gè)字符串"nextSuccess"是否需要向后傳遞
包裝職責(zé)鏈chain
const order500 = function(orderType, pay, stock) { if(orderType === 1 && pay === true) { console.log("500定金,100優(yōu)惠券") } else { return "nextSuccess" } } const order200 = function(orderType, pay, stock) { if (orderType === 2 && pay === true) { console.log("200定金,返50優(yōu)惠券") } else { return "nextSuccess" } } const orderNormal = function(orderType, pay, stock) { if (stock > 0) { console.log("普通購(gòu)買") } else { console.log("手機(jī)庫(kù)存不足") } } const Chain = function(fn) { this.fn = fn; this.successor = null; } Chain.prototype = { setNextSuccessor: function(successor) { return this.successor = successor; }, passRequest: function() { let ret = this.fn.apply(this, arguments); if (ret === "nextSuccessor") { return this.successor && this.successor.passRequest.apply(this.successor, arguments) } return ret; } } // 包裝職責(zé)鏈節(jié)點(diǎn) const chainOrder500 = new Chain(order500); const chainOrder200 = new Chain(order200); const chainOrderNormal = new Chain(orderNormal); // 指定職責(zé)鏈順序 chainOrder500.setNextSuccessor(chainOrder200); chainOrder200.setNextSuccessor(chainOrderNormal) // 應(yīng)用 chainOrder500.passRequest(1, true, 500); // 500定金,100優(yōu)惠券 chainOrder500.passRequest(1, false, 0) // 庫(kù)存不足
職責(zé)鏈的缺點(diǎn):
職責(zé)鏈?zhǔn)钩绦蛑卸嗔艘恍┕?jié)點(diǎn)對(duì)象,可能在某一次的請(qǐng)求傳遞過程中,大部分節(jié)點(diǎn)沒有起到實(shí)質(zhì)性的作用,它們的作用僅僅讓請(qǐng)求傳遞下去,從性能方面考慮我們應(yīng)該避免過長(zhǎng)的職責(zé)鏈帶來的性能損耗。
在之前我們使用了AOP裝飾函數(shù),實(shí)現(xiàn)裝飾者的模式。
同樣 這里我們可以使用AOP實(shí)現(xiàn)職責(zé)鏈
改寫之前的Function.prototype.after函數(shù)
Function.prototype.after = function(fn) { let self = this; return function() { let ret = self.apply(this, arguments); if (ret === "nextSuccessor") { return fn.apply(this, arguments) } return ret; } } // 指定順序 let order = order500.after(order200).after(orderNormal); order(1, true, 50) // 500定金,100優(yōu)惠券
去掉了chain類,整個(gè)邏輯也變得更加清晰了,同樣這種方式也不適合太長(zhǎng)的鏈條。
狀態(tài)模式狀態(tài)模式指的是:允許一個(gè)對(duì)象在其內(nèi)部狀態(tài)改變時(shí)改變它的行為,對(duì)象看起來似乎修改了它的類。
狀態(tài)模式的優(yōu)點(diǎn):
狀態(tài)模式定義了狀態(tài)與行為之間的關(guān)系,并將它們封裝在一個(gè)類里。通過增加新的狀態(tài)類,很容易增加新的狀態(tài)和轉(zhuǎn)換。
避免Context無限膨脹,狀態(tài)切換的邏輯被分布在狀態(tài)類中,也去掉了Context中原來過多的條件分支。
用對(duì)象代替字符串來記錄當(dāng)前狀態(tài),使得狀態(tài)的切換更加一目了然。
Context中的請(qǐng)求動(dòng)作和狀態(tài)類中封裝的行為可以非常容易地獨(dú)立變化而互不影響。
使用javascript版本的狀態(tài)機(jī),實(shí)現(xiàn)開燈與關(guān)燈例子:
let delegate = function(client, delegation) { return { buttonWasPressed: function() { // 將客戶端操作委托給delegation對(duì)象 return delegation.buttonWasPressed.apply(client, arguments) } } } let FSM = { off: { buttonWasPressed: function() { console.log("關(guān)燈") this.button.innerHTML = "下次按我是開燈"; this.currState = this.onState; } }, on: { buttonWasPressed: function() { console.log("開燈"); this.button.innerHTML = "下次按我是關(guān)燈" this.currState = this.offState; } } } let Light = function() { this.offState = delegate(this, FSM.off); this.onState = delegate(this, FSM.on); this.currState = this.offState; // 設(shè)置初始狀態(tài)為關(guān)閉狀態(tài) this.button = null; } Light.prototype = { init () { let button = document.getElementById("button"); let self = this; button.innerHTML = "已關(guān)燈"; this.button = document.body.appendChild(button); this.button.onclick = function () { self.currState.buttonWasPressed() } } } let light = new Light() light.init()結(jié)語(yǔ)
寫這篇文章主要意圖在于以前也看過javascript設(shè)計(jì)模式這本書,但可能在寫代碼的時(shí)候一般只會(huì)想到文章開頭的三個(gè)原則,但具體如何通過比較優(yōu)雅的代碼去滿足三個(gè)原則是比較困難的,最近又重新看了一遍這本書,為了加深自己的印象,將一些比較常用的模式通過自己去描述的方式呈現(xiàn)出來,也能分享給廣大的搬磚同學(xué)~, 如果覺得讀完文章后有所收獲,請(qǐng)點(diǎn)贊和收藏給予一丟丟鼓勵(lì),haha
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/106954.html
摘要:前言月份開始出沒社區(qū),現(xiàn)在差不多月了,按照工作的說法,就是差不多過了三個(gè)月的試用期,準(zhǔn)備轉(zhuǎn)正了一般來說,差不多到了轉(zhuǎn)正的時(shí)候,會(huì)進(jìn)行總結(jié)或者分享會(huì)議那么今天我就把看過的一些學(xué)習(xí)資源主要是博客,博文推薦分享給大家。 1.前言 6月份開始出沒社區(qū),現(xiàn)在差不多9月了,按照工作的說法,就是差不多過了三個(gè)月的試用期,準(zhǔn)備轉(zhuǎn)正了!一般來說,差不多到了轉(zhuǎn)正的時(shí)候,會(huì)進(jìn)行總結(jié)或者分享會(huì)議!那么今天我就...
摘要:特意對(duì)前端學(xué)習(xí)資源做一個(gè)匯總,方便自己學(xué)習(xí)查閱參考,和好友們共同進(jìn)步。 特意對(duì)前端學(xué)習(xí)資源做一個(gè)匯總,方便自己學(xué)習(xí)查閱參考,和好友們共同進(jìn)步。 本以為自己收藏的站點(diǎn)多,可以很快搞定,沒想到一入?yún)R總深似海。還有很多不足&遺漏的地方,歡迎補(bǔ)充。有錯(cuò)誤的地方,還請(qǐng)斧正... 托管: welcome to git,歡迎交流,感謝star 有好友反應(yīng)和斧正,會(huì)及時(shí)更新,平時(shí)業(yè)務(wù)工作時(shí)也會(huì)不定期更...
摘要:前端每周清單專注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開發(fā)教程工程實(shí)踐深度閱讀開源項(xiàng)目巔峰人生等欄目。背后的故事本文是對(duì)于年之間世界發(fā)生的大事件的詳細(xì)介紹,闡述了從提出到角力到流產(chǎn)的前世今生。 前端每周清單專注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn);分為新聞熱點(diǎn)、開發(fā)教程、工程實(shí)踐、深度閱讀、開源項(xiàng)目、巔峰人生等欄目。歡迎...
摘要:忍者級(jí)別的函數(shù)操作對(duì)于什么是匿名函數(shù),這里就不做過多介紹了。我們需要知道的是,對(duì)于而言,匿名函數(shù)是一個(gè)很重要且具有邏輯性的特性。通常,匿名函數(shù)的使用情況是創(chuàng)建一個(gè)供以后使用的函數(shù)。 JS 中的遞歸 遞歸, 遞歸基礎(chǔ), 斐波那契數(shù)列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執(zhí)行機(jī)制 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機(jī)制,如果...
摘要:在他的重學(xué)前端課程中提到到現(xiàn)在為止,前端工程師已經(jīng)成為研發(fā)體系中的重要崗位之一。大部分前端工程師的知識(shí),其實(shí)都是來自于實(shí)踐和工作中零散的學(xué)習(xí)。一基礎(chǔ)前端工程師吃飯的家伙,深度廣度一樣都不能差。 開篇 前端開發(fā)是一個(gè)非常特殊的行業(yè),它的歷史實(shí)際上不是很長(zhǎng),但是知識(shí)之繁雜,技術(shù)迭代速度之快是其他技術(shù)所不能比擬的。 winter在他的《重學(xué)前端》課程中提到: 到現(xiàn)在為止,前端工程師已經(jīng)成為研...
閱讀 2464·2021-11-23 09:51
閱讀 523·2019-08-30 13:59
閱讀 1829·2019-08-29 11:20
閱讀 2534·2019-08-26 13:41
閱讀 3244·2019-08-26 12:16
閱讀 733·2019-08-26 10:59
閱讀 3326·2019-08-26 10:14
閱讀 603·2019-08-23 17:21