心里按摩部分


我們寫" />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網(wǎng)站運營 > 前端進階之路:1.5w字整理23種前端設計模式

前端進階之路:1.5w字整理23種前端設計模式

時間:2023-10-07 18:18:01 | 來源:網(wǎng)站運營

時間:2023-10-07 18:18:01 來源:網(wǎng)站運營

前端進階之路:1.5w字整理23種前端設計模式:我們開發(fā)人員經(jīng)常會說:"Talk is cheap, show me the code"。要想寫出令人賞心悅目的代碼,我覺得是否使用了合理的設計模式起了至關重要的作用。


心里按摩部分


我們寫的代碼就是我們的名片,但是拿出如果是沒有經(jīng)過設計的代碼,不僅讓人讀起來費勁,還會讓人質(zhì)疑我們的能力。


還有阻礙我們進步的是當你去讀別人的框架或者源碼的時候,作者往往用到了大量的設計模式,如果我們不能對設計模式足夠敏感的話,很可能會浪費大量的時間,甚至一直卡在一個地方出不來,而且設計模式也在面試的過程中占了很大的一部分。


所以不論是為了以后code review的時候你的代碼倍兒有面兒,還是你自己的成長和職業(yè)發(fā)展,我們都應該好好的學一學設計模式。廢話不在多說,下面讓我們來一起開始正式的學習。


設計原則與思想



一切拋開設計原則講設計模式的行為都是無恥渣男耍流氓的行為,只會讓你爽一時,永遠也不能擁有自己的幸福,最后掉入一個無底洞(咳咳,跑題了)。


總之我們不能手里有把錘子,看哪里都是釘子,要知道為什么要使用這種設計模式,以及解決了什么問你題,有哪些應用場景。這才是關鍵的關鍵,如果你清晰的明白了這些設計原則,甚至可以根據(jù)場景組裝你自己的設計模式。


廢話不多說,我們應該也大體了解一些比較經(jīng)典的設計模式,比如 SOLID、KISS、YAGNI、DRY、LOD 等。我們接下來會一一的進行介紹。



SOLID 原則


SOLID 原則并非單純的 1 個原則,而是由 5 個設計原則組成的,它們分別是:單一職責原則、開閉原則、里式替換原則、接口隔離原則和依賴反轉(zhuǎn)原則,依次對應 SOLID 中的 S、O、L、I、D 這 5 個英文字母。我們來分別看一下。


SRP-單一職責原則



OCP-開閉原則


LSP-里式替換原則


看定義還是有點抽象,我們來舉個例子看一下

class GetUser { constructor(id) { this.id = id } getInfo() { const params = {id: this.id} //...code here }}class GetVipUser extends GetUser { constructor(id, vipLevel) { super(id) this.id = id this.level = vipLevel } getInfo() { const params = {id: this.id} if (this.level != void 0) { params.level = this.level } super.getInfo(params) }}class Demo { getUser(user) { console.log(user.getInfo()) }}// 里式替換原則const u = new Demo()u.getUser(new GetUser())u.getUser(new GetVipUser())我們看到GetVipUser的設計是符合里式替換原則的,其可以替換父類出現(xiàn)的任何位置,并且原來代碼的邏輯行為不變且正確性也沒有被破壞。


很多人看到這里就會說,你這不就是利用了類的多態(tài)特性嗎?確實,他倆看著確實有點像,但是實際上是完全不同的兩回事,我們把上面的代碼稍微改造一下。


class GetUser { constructor(id) { this.id = id } getInfo() { const params = {id: this.id} //...code here }}class GetVipUser extends GetUser { constructor(id, vipLevel) { super(id) this.id = id this.level = vipLevel } getInfo() { const params = {id: this.id} if (this.level == void 0) { throw new Error('level should not undefind') } super.getInfo(params) }}class Demo { getUser(user) { console.log(user.getInfo()) }}// 里式替換原則const u = new Demo()u.getUser(new GetUser())u.getUser(new GetVipUser())改動之后我們可以很清晰的看到,父類在運行時是不會出錯的,但是子類當沒有接受level的時候回拋出錯誤,整個程序的邏輯和父類產(chǎn)生了區(qū)別,所以是不符合里式替換原則的。


稍微總結一下。雖然從定義描述和代碼實現(xiàn)上來看,多態(tài)和里式替換有點類似,但它們關注的角度是不一樣的。多態(tài)是面向?qū)ο缶幊痰囊淮筇匦裕彩敲嫦驅(qū)ο缶幊陶Z言的一種語法。它是一種代碼實現(xiàn)的思路。而里式替換是一種設計原則,是用來指導繼承關系中子類該如何設計的,子類的設計要保證在替換父類的時候,不改變原有程序的邏輯以及不破壞原有程序的正確性。

里式替換原則是用來指導,繼承關系中子類該如何設計的一個原則。理解里式替換原則,最核心的就是理解“design by contract,按照協(xié)議來設計”這幾個字。父類定義了函數(shù)的“約定”(或者叫協(xié)議),那子類可以改變函數(shù)的內(nèi)部實現(xiàn)邏輯,但不能改變函數(shù)原有的“約定”。這里的約定包括:函數(shù)聲明要實現(xiàn)的功能;對輸入、輸出、異常的約定;甚至包括注釋中所羅列的任何特殊說明。


LSP-接口隔離原則





這一原則和單一職責原則有點類似,只不過它更側(cè)重于接口。


DIP-依賴反轉(zhuǎn)原則


KISS原則
關于KISS原則英文描述有好幾個版本





YAGNI原則
YAGNI 原則的英文全稱是:You Ain’t Gonna Need It。直譯就是:你不會需要它。這條原則也算是萬金油了。當用在軟件開發(fā)中的時候,它的意思是:不要去設計當前用不到的功能;不要去編寫當前用不到的代碼。實際上,這條原則的核心思想就是:不要做過度設計。


DRY原則
它的英文描述為:Don’t Repeat Yourself。中文直譯為:不要重復自己。將它應用在編程中,可以理解為:不要寫重復的代碼。 看似簡單,實際上我們工作中不自覺的寫了大量重復的代碼,比如


迪米特法則


單從這個名字上來看,我們完全猜不出這個原則講的是什么。不過,它還有另外一個更加達意的名字,叫作最小知識原則,英文翻譯為:The Least Knowledge Principle。 通俗的講就是: 不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量只依賴必要的接口(也就是定義中的“有限知識”)。 迪米特法則是實現(xiàn)高內(nèi)聚,松耦合的法寶。那么什么是高內(nèi)聚和松耦合呢?


所謂高內(nèi)聚,就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中。相近的功能往往會被同時修改,放到同一個類中,修改會比較集中,代碼容易維護。

所謂松耦合是說,在代碼中,類與類之間的依賴關系簡單清晰。即使兩個類有依賴關系,一個類的代碼改動不會或者很少導致依賴類的代碼改動。

不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量只依賴必要的接口。迪米特法則是希望減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統(tǒng)的其他部分。一旦發(fā)生變化,需要了解這一變化的類就會比較少。


總結

說了這么多,我們應該熟練的掌握這些原則,有了這些原則我們才能明白后面的設計模式范式到底是遵循了什么思想,要解決什么問題。并且要時刻提醒自己,設計模式不是重點,寫出高質(zhì)量的代碼才是我們要到達的彼岸。



設計模式與范式


設計模式分三個大類: 創(chuàng)建型模式, 結構型模式,行為型模式



創(chuàng)建型設計模式



創(chuàng)建型模式中,單例模式,工廠模式(又分為簡單工廠和抽象工廠),和原型模式是比較常用的,建造者模式用的不太多,了解下就好。


單例模式
單例設計模式(Singleton Design Pattern)理解起來非常簡單。一個類只允許創(chuàng)建一個對象(或者實例),那這個類就是一個單例類,這種設計模式就叫作單例設計模式,簡稱單例模式。


單例模式的實現(xiàn)也比較簡單,下面給出了兩種實現(xiàn)方法

//方法一class GetSeetingConfig { static instance = null constructor() { console.log('new') } getConfig() { //... } static getInstance () { if (this.instance == void 0) { this.instance = new GetSeetingConfig() } return this.instance }}const seeting1 = GetSeetingConfig.getInstance()const seeting2 = GetSeetingConfig.getInstance()//兩次只打印一次newseeting1 === seeting2 // true//方法二class GetSeetingConfig { constructor() { console.log('new') } getConfig() { //... }}GetSeetingConfig.getInstance = (function() { let instance return function() { if (!instance){ instance = new GetSeetingConfig() } return instance }})()const seeting1 = GetSeetingConfig.getInstance()const seeting2 = GetSeetingConfig.getInstance()//兩次只打印一次newseeting1 === seeting2 // true優(yōu)點:


缺點:


經(jīng)典場景:


工廠模式
我們從字面意思上來理解工廠,對于消費者來說,我們不關心你的生產(chǎn)流程,關心的是最終的產(chǎn)品。


所以為了讓代碼邏輯更加清晰,可讀性更好,我們要善于將功能獨立的代碼塊進行封裝一個職責單一的類或者模塊,這種基于抽象的思維就是工廠模式的來源。
廠模式又分為簡單工廠模式,工廠方法模式和抽象工廠模式。


簡單工廠模式




//簡單工廠模式class User { constructor(role, name) { this.name = name; this.role = role }}class Admin { constructor(role, name) { this.name = name; this.role = role }}class SuperAdmin { constructor(role, name) { this.name = name; this.role = role }}class RoleFactory { static createUser(role) { if (role === 'user') { return new User(role,'用戶') } else if (role === 'admin') { return new Admin(role, '管理員') } else if (role === 'superadmin') { return new SuperAdmin(role, '超級管理員') } }}const user = RoleFactory.createUser('user'')簡單工廠的優(yōu)點在于,你只需要一個正確的參數(shù),就可以獲取到你所需要的對象,而無需知道其創(chuàng)建的具體細節(jié)。但是當內(nèi)部邏輯變得很復雜這個函數(shù)將會變得很龐大并且難以維護。


工廠方法模式
所以當一個簡單工廠變得過于復雜時,我們可以考慮用工廠方法來代替它。工廠方法的核心是將實際創(chuàng)建對象的工作推遲到子類中。



class UserFactory { constructor(role, name) { this.name = name; this.role = role; } init() { //我們可以把簡單工廠中復雜的代碼都拆分到每個具體的類中 // code here //... return new User(this.role, this.name) }}class AdminFactory { constructor(role, name) { this.name = name; this.role = role; } init() { //我們可以把簡單工廠中復雜的代碼都拆分到每個具體的類中 // code here //... return new Admin(this.role, this.name) }}class SuperAdminFactory { constructor(role, name) { this.name = name; this.role = role; } init() { //我們可以把簡單工廠中復雜的代碼都拆分到每個具體的類中 // code here //... return new SuperAdmin(this.role, this.name) }}class RoleFactory { static createUser(role) { if (role === 'user') { return new UserFactory(role,'用戶') } else if (role === 'admin') { return new AdminFactory(role, '管理員') } else if (role === 'superadmin') { return new SuperAdminFactory(role, '超級管理員') } }}const user = RoleFactory.createUser('user'')那什么時候該用工廠方法模式,而非簡單工廠模式呢?


之所以將某個代碼塊剝離出來,獨立為函數(shù)或者類,原因是這個代碼塊的邏輯過于復雜,剝離之后能讓代碼更加清晰,更加可讀、可維護。但是,如果代碼塊本身并不復雜,就幾行代碼而已,我們完全沒必要將它拆分成單獨的函數(shù)或者類。

基于這個設計思想,當對象的創(chuàng)建邏輯比較復雜,不只是簡單的 new 一下就可以,而是要組合其他類對象,做各種初始化操作的時候,我們推薦使用工廠方法模式,將復雜的創(chuàng)建邏輯拆分到多個工廠類中,讓每個工廠類都不至于過于復雜。而使用簡單工廠模式,將所有的創(chuàng)建邏輯都放到一個工廠類中,會導致這個工廠類變得很復雜。


抽象工廠模式
在簡單工廠和工廠方法中,類只有一種分類方式。上面的例子中我們根據(jù)用和的角色來劃分的,但是如果根據(jù)業(yè)務需要,我們也需要對用戶注冊時用的手機號或者郵箱也要進行劃分的話。如果用工廠方法我們就需要上面三種每種又分為手機號或郵箱的工廠,總共9中,要是再增加一種分類形式,我們可以發(fā)現(xiàn)它是指數(shù)型增長的趨勢。我們的工廠總有一天會爆炸的。 抽象工廠就是針對這種非常特殊的場景而誕生的。我們可以讓一個工廠負責創(chuàng)建多個不同類型的對象,而不是只創(chuàng)建一種對象。這樣就可以有效地減少工廠類的個數(shù)。


class Factory { createUserParser(){ thorw new Error('抽象類只能繼承,不能實現(xiàn)') } createLoginParser(){ thorw new Error('抽象類只能繼承,不能實現(xiàn)') }}class UserParser extends Factory { createUserParser(role, name) { return new UserFactory(role, name) } createLoginParser(type) { if (type === 'email'){ return new UserEmail() } else if (type === 'phone') { return new UserPhone() } }}class AdminParser extends Factory { createUserParser(role, name) { return new AdminFactory(role, name) } createLoginParser(type) { if (type === 'email'){ return new AdminEmail() } else if (type === 'phone') { return new AdminPhone() } }}class SuperAdminParser extends Factory { createUserParser(role, name) { return new SuperAdminFactory(role, name) } createLoginParser(type) { if (type === 'email'){ return new SuperAdminEmail() } else if (type === 'phone') { return new SuperAdminPhone() } }}總結


除了剛剛提到的這幾種情況之外,如果創(chuàng)建對象的邏輯并不復雜,那我們就直接通過 new 來創(chuàng)建對象就可以了,不需要使用工廠模式。
現(xiàn)在,我們上升一個思維層面來看工廠模式,它的作用無外乎下面這四個。這也是判斷要不要使用工廠模式的最本質(zhì)的參考標準。


建造者模式


將一個復雜的對象分解成多個簡單的對象來進行構建,將復雜的構建層與表示層分離,使得相同的構建過程可以創(chuàng)建不同的表示的模式便是建造者模式。
這樣說還是有點抽象,我們來看一個具體的例子,比如我們要創(chuàng)建一個CaKe類,這個類需要傳name,color, shape,sugar這四個參數(shù)。這當然難不倒我們,只需要



class Cake { constructor(name, color, shape, suger) { this.name = name; this.color = color; this.shape = shape; this.suger = suger; }}new Cake('cake', 'white', 'circle', '30%')現(xiàn)在,Cake 只有 4 個可配置項,對應到構造函數(shù)中,也只有 4 個參數(shù),參數(shù)的個數(shù)不多。但是,如果可配置項逐漸增多,變成了 8 個、10 個,甚至更多,那繼續(xù)沿用現(xiàn)在的設計思路,構造函數(shù)的參數(shù)列表會變得很長,代碼在可讀性和易用性上都會變差。在使用構造函數(shù)的時候,我們就容易搞錯各參數(shù)的順序,傳遞進錯誤的參數(shù)值,導致非常隱蔽的 bug。


這個時候,我們還有一個方法,就是給每個屬性添加set方法,將必填的屬性放到構造函數(shù)中,不是必填的向外暴露set方法,讓使用者選擇自主填寫或者不填寫。(比如我們的name和color是必填的,其余不必填)

class Cake { consotructor(name, color) { this.name = name; this.color = color; } validName() { if(this.name == void 0) { console.log('name should not empty') return false } return true } validColor() { if (this.color == void 0) { console.log('color should not empty') true } return true } setShape(shape) { if (this.validName() && this.validColor()) { this.shape = shape; } } setSugar(sugar) { if (this.validName() && this.validColor()) { this.sugar = sugar; } } //...}至此,我們?nèi)匀粵]有用到建造者模式,通過構造函數(shù)設置必填項,通過 set() 方法設置可選配置項,就能實現(xiàn)我們的設計需求。 但是我們再增加一下難度


class Cake { constructor(name, color, shape, suger) { this.name = name; this.color = color; this.shape = shape; this.suger = suger; }}class CakeBuilder { valid() { //valid all params... } setName() { this.valid() //... return this; } setColor() { this.valid() //... return this; } setShage() { this.valid() //... return this; } setSuger() { this.valid() //... return this; } build() { const cake = new Cake() cake.shape = this.setShape() cake.suger = this.setSuger() cake.name = this.setName() cake.color = this.setColor() return cake }}const cake1 = new CakeBuilder() .setName('cake') .setColor('yellow') .setShape('heart') .setSugar('70%') .builder()//我們還可以把這長長的鏈式調(diào)用封裝起來,也就是指導者function diractor(builder) { return builder .setName('cake') .setColor('yellow') .setShape('heart') .setSugar('70%') .builder()}const cakeBuilder = new CakeBuilder()const cake2 = diractor(cakeBuilder)Cake類中的成員變量,要在 Builder 類中重新再定義一遍,可以看出,建造者模式的使用有且只適合創(chuàng)建極為復雜的對象。在前端的實際業(yè)務中,在沒有這類極為復雜的對象的創(chuàng)建時,還是應該直接使用對象字面或工廠模式等方式創(chuàng)建對象。


原型模式


如果對象的創(chuàng)建成本比較大,而同一個類的不同對象之間差別不大(大部分字段都相同),在這種情況下,我們可以利用對已有對象(原型)進行復制(或者叫拷貝)的方式來創(chuàng)建新對象,以達到節(jié)省創(chuàng)建時間的目的。這種基于原型來創(chuàng)建對象的方式就叫作原型設計模式(Prototype Design Pattern),簡稱原型模式。

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()對于前端程序員來說,原型模式是一種比較常用的開發(fā)模式。這是因為,有別于 Java、C++ 等基于類的面向?qū)ο缶幊陶Z言,JavaScript 是一種基于原型的面向?qū)ο缶幊陶Z言。即便 JavaScript 現(xiàn)在也引入了類的概念,但它也只是基于原型的語法糖而已。


結構型設計模式
結構型模式又分為7類,其中代理模式,裝飾者模式,適配器模式和橋接模式用的比較多。


代理模式

代理模式(Proxy Design Pattern)的原理和代碼實現(xiàn)都不難掌握。它在不改變原始類(或叫被代理類)代碼的情況下,通過引入代理類來給原始類附加功能。代理模式在前端中比較常用的虛擬代理和緩存代理。


虛擬代理
我們用虛擬代理來實現(xiàn)一個圖片的懶加載

class MyImg { static imgNode = document.createElement("img") constructor(selector) { selector.appendChild(this.imgNode); } setSrc(src) { this.imgNode = src }}const img = new MyImg(document.body)img.setSrc('xxx')先看一下上面這段代碼,定義了一個MyImg類,接收一個選擇器,然后在這個選擇器下面創(chuàng)建一個img標簽并且暴露一個setSrc方法。


如果網(wǎng)速慢,圖片又非常大的話,拿這個標簽的占位剛開始是白屏的。所以我們會用到圖片預加載的方式。


虛擬代理的特點是,代理類和真正的類都暴露了痛仰的接口,這樣對于調(diào)用者來說是無感的。


class ProxyMyImg { static src = 'xxx本地預覽圖地址loading.gif' constructor(selector) { this.img = new Image this.myImg = new MyImg(selector) this.myImg.setSrc(this.src) } setSrc(src) { this.img.src = src this.img.onload = () => { this.myImg.setSrc(src) } }}const img = new ProxyMyImg(document.body)img.setSrc('xxx')ProxyMyImg控制了客戶對MyImg的訪問,并且在此過程中加入一些額外的操作,比如在圖片加載好之前,先把img節(jié)點的src設置為一張本地的loading圖片。


這樣做的好處是把添加img節(jié)點和設置預加載給解耦了,每個類都去一個任務,這是符合單一職責原則的。如果有一天網(wǎng)速足夠快了,完全不需要預加載,我們直接去掉代理就可以了,這也是符合開閉原則的。


緩存代理
緩存代理可以為一些開銷大的運算結果提供暫時的緩存,在下次運算時,如果傳遞進來的參數(shù)跟之前一致,則可以直接返回緩存好的運算結果。
比如我們有一個計算乘積的函數(shù)



const mult = (...args) => { console.log('multing...') let res = 1 args.forEach(item => { res*=item }) return item}mult(2,3) //6mult(2,3,5)//30加入緩存代理函數(shù)

const mult = (...args) => { console.log('multing...') let res = 1 args.forEach(item => { res*=item }) return res }const proxyMult = (() => { const cache = {} return (...args) => { const key = [].join.call(args, ',') if (key in cache) { return cache[args] } return cache[key] = mult.apply(null, args) }})()proxyMult(1,2,3,4)// multing... 24proxyMult(1,2,3,4)//24
  1. Proxy
    ES6新增了代理類Proxy
    語法
    const p = new Proxy(target, handler)
    target 要使用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數(shù)組,函數(shù),甚至另一個代理)。
    handler 一個通常以函數(shù)作為屬性的對象,各屬性中的函數(shù)分別定義了在執(zhí)行各種操作時代理 p 的行為。
    關于更多的詳情請看 developer.mozilla.org/zh-CN/docs/…
    下面我們用Proxy來實現(xiàn)一下緩存代理的例子
const mult = (args) => { console.log('multing...') let res = 1 args.forEach(item => { res*=item }) return res}const handler = { cache: {}, apply: function(target, thisArg, args) { const key = [].join.call(args, ',') if(key in this.cache) { return this.cache[key] } return this.cache[key] = target(args) }}const proxyMult = new Proxy(mult, handler)proxyMult(1,2,3,4)//multing...//24proxyMult(1,2,3,4)//24

裝飾者模式

裝飾者模式可以動態(tài)地給某個對象添加一些額外的職責,而不會影響從這個類中派生的其他對象。

class Plan { fire() { console.log('發(fā)射子彈') }}class PlanDecorator { constructor(plan) { this.plan = plan } fire() { this.plan.fire() console.log('發(fā)射導彈') }}const plan = new Plan()const newPlan = new PlanDecorator(plan)newPlan.fire() //發(fā)射子彈 發(fā)射導彈如果你熟悉TypeScript,那么他的代碼結構就是這樣的

interface IA { init: () => void}class A implements IA { public init() { //... }}class ADecorator implements IA { constructor (a: IA) { this.a = a } init() { // 功能增強代碼 a.init() // 功能增強代碼 }}著名的AOP就是通過裝飾者模式來實現(xiàn)的

before

Function.prototype.before = function(beforeFn) { const _this = this //保存原函數(shù)的引用 return function() {// 返回包含了原函數(shù)和新函數(shù)的"代理函數(shù)" beforeFn.apply(this, arguments)// 執(zhí)行新函數(shù),修正this return _this.apply(this, arguments) // 執(zhí)行原函數(shù)并返回原函數(shù)的執(zhí)行結果,this不被劫持 }}after

Function.prototype.after = function(afterFn) { const _this = this return function() { const res = _this.apply(this, arguments) afterFn.apply(this, arguments) return res }}around(環(huán)繞通知)

Function.prototype.around = function(beforeFn, aroundFn) { const _this = this return function () { return _this.before(beforeFn).after(aroundFn).apply(this, arguments)// 利用之前寫的before 和after 來實現(xiàn)around }} 測試

const log = (val) => { console.log(`日志輸出${val}`)}const beforFn = () => { console.log(`日志輸出之前先輸出${new Date().getTime()}`)}const afterFn = () => { console.log(`日志輸出之前再輸出${new Date().getTime()}`)}const preLog = log.before(beforFn)const lastLog = log.after(afterFn)const aroundLog = log.around(beforeFn, afterFn)preLog(11)lastLog(22)aroundLog(33)

當AOP遇到裝飾器

ES7中增加了對裝飾器的支持,它就是用來修改類的行為,或者增強類,這使得在js中使用裝飾者模式變得更便捷

class User { @checkLogin getUserInfo() { console.log('獲取用戶信息') }}// 檢查用戶是否登錄function checkLogin(target, name, descriptor) { let method = descriptor.value descriptor.value = function (...args) { // 校驗方法,假設這里可以獲取到用戶名/密碼 if (validate(args)) { method.apply(this, args) } else { console.log('沒有登錄,即將跳轉(zhuǎn)到登錄頁面...') } }}let user = new User()user.getUserInfo()還有比較典型的在React中的高階組件

function HOCDecorator(WrappedComponent){ return class HOC extends Component { render(){ const newProps = {param: 'HOC'}; return <div> <WrappedComponent {...this.props} {...newProps}/> </div> } }}@HOCDecoratorclass OriginComponent extends Component { render(){ return <div>{this.props.param}</div> }}如果你熟悉mobx的話,你會發(fā)現(xiàn)里面的功能都支持裝飾器,當然我們也可以在Redux中使用裝飾器來實現(xiàn)connect函數(shù),這都是很方便的。 具體可以看阮一峰老師的講解 es6.ruanyifeng.com/#docs/decor…


適配器模式
適配器模式的英文翻譯是 Adapter Design Pattern。顧名思義,這個模式就是用來做適配的,它將不兼容的接口轉(zhuǎn)換為可兼容的接口,讓原本由于接口不兼容而不能一起工作的類可以一起工作。對于這個模式,有一個經(jīng)常被拿來解釋它的例子,就是 USB 轉(zhuǎn)接頭充當適配器,把兩種不兼容的接口,通過轉(zhuǎn)接變得可以一起工作。

class GooleMap { show() { console.log('渲染地圖') }}class BaiduMap { display() { console.log('渲染地圖') }}class GaodeMap { show() { console.log('渲染地圖') }}// 上面三個類,如果我們用多態(tài)的思想去開發(fā)的話是很難受的,所以我們通過適配器來統(tǒng)一接口class BaiduAdaapterMap { show() { return new BaiduMap().display() }}以適配器模式應用場景一般為


橋接模式


舉個很簡單的例子,現(xiàn)在有兩個緯度 Car 車 (奔馳、寶馬、奧迪等) Transmission 檔位類型 (自動擋、手動擋、手自一體等) 按照繼承的設計模式,Car是一個基類,假設有M個車品牌,N個檔位一共要寫 M乘N 個類去描述所有車和檔位的結合。而當我們使用橋接模式的話,我首先new一個具體的Car(如奔馳),再new一個具體的Transmission(比如自動檔)。 那么這種模式只有M+N個類就可以描述所有類型,這就是M乘N的繼承類爆炸簡化成了M+N組合。



class Car { constructor(brand) { this.brand = brand } speed() { //... }}class Transmission { constructor(trans) { this.trans = trans } action() { ... }}class AbstractCar { constructor(car, transmission) { this.car = car this.transmission } run () { this.car.speed() this.traansmission.action() //... }}門面模式


門面模式,也叫外觀模式,英文全稱是 Facade Design Pattern。在 GoF 的《設計模式》一書中,門面模式是這樣定義的:門面模式為子系統(tǒng)提供一組統(tǒng)一的接口,定義一組高層接口讓子系統(tǒng)更易用。


假設有一個系統(tǒng) A,提供了 a、b、c、d 四個接口。系統(tǒng) B 完成某個業(yè)務功能,需要調(diào)用 A 系統(tǒng)的 a、b、d 接口。利用門面模式,我們提供一個包裹 a、b、d 接口調(diào)用的門面接口 x,給系統(tǒng) B 直接使用。


如果是后臺同學的話,說明接口粒度太細了,我們可以要求其將abd接口封裝為一個接口供我們使用,這會提高一部分性能。但是由于各種原因后臺不改,我們也可以將abd接口封裝在一起,這也使得我們的代碼具有高內(nèi)聚,低耦合的特性。


舉個最簡單的例子


const myEvent = { // ... stop: e => { e.stopPropagation(); e.preventDefault(); }}合模式


組合模式的定義: 將一組對象組織(Compose)成樹形結構,以表示一種“部分 - 整體”的層次結構。 組合模式的應用前提是他的數(shù)據(jù)需要是樹形結構。
假設我們有這樣一個需求:設計一個類來表示文件系統(tǒng)中的目錄,能方便地實現(xiàn)下面這些功能:


class FileSystemNode { constructor(path) { this.path = path } countNumOfFiles() {} countSizeOfFiles() {} getPath() { return this.path }}class File extends FileSystemNode { constructor(path) { super(path) } countNumOfFiles () { return 1 } countSizeOfFiles() { //利用NodejsApi通過路徑獲取文件... }}class Directory extends FileSystemNode{ constructor(path) { super(path) this.fileList = [] } countNumOfFiles () { //... } countSizeOfFiles() { //... } addSubNode(fileOrDir) { this.fileList.push(fileOrDir) } removeSubNode(fileOrDir) { return this.fileList.filter(item => item !== fileOrDir) } }//如果我們要表示/** */ */leon */leon/aa.txt */leon/bb */leon/bb/cc.js const root = new Directory('/')const dir_leon = new Directory('/leon/')root.addSubNode(leon)const file_aa = new File('/leon/aa.txt')const dir_bb = new Directory('leon/bb')dir_leon.addSubNode(file_aa)dir_leon.addSubNode(dir_bb)const file_cc = new File('/leon/bb/cc.js')dir_bb.addSubNode(file_cc)組合模式不好的一點是有可能一不小心就創(chuàng)建了大量的對象而降低性能或難以維護,所以我們接下來講的享元模式就是用來解決這個問題的。


享元模式
所謂“享元”,顧名思義就是被共享的單元。享元模式的意圖是復用對象,節(jié)省內(nèi)存,前提是享元對象是不可變對象。


定義中的“不可變對象”指的是,一旦通過構造函數(shù)初始化完成之后,它的狀態(tài)(對象的成員變量或者屬性)就不會再被修改了。所以,不可變對象不能暴露任何 set() 等修改內(nèi)部狀態(tài)的方法。


具體來講,當一個系統(tǒng)中存在大量重復對象的時候,如果這些重復的對象是不可變對象,我們就可以利用享元模式將對象設計成享元,在內(nèi)存中只保留一份實例,供多處代碼引用。這樣可以減少內(nèi)存中對象的數(shù)量,起到節(jié)省內(nèi)存的目的。實際上,不僅僅相同對象可以設計成享元,對于相似對象,我們也可以將這些對象中相同的部分(字段)提取出來,設計成享元,讓這些大量相似對象引用這些享元。


我們來看個例子,假如要做一個簡單的富文本編輯器(只需要記錄文字和格式)。

class CharacterStyle{ constructor(font, size, color) { this.font = font this.size = size this.color = color } equals(obj) { return this.font === obj.font && this.size === obj.size && this.color = obj.color }}class CharacterStyleFactory { static styleList = [] getStyle(font, size, color) { const newStyle = new CharacterStyle(font, size, color) for(let i = 0, style; style = this.styleList[i++];) { if (style.equals(newStyle)) { return style } } CharacterStyleFactory.styleList.push(newStyle) return newStyle }}class Character { constructor(c, style) { this.c = c this.style = style }}class Editor { static chars = [] appendCharacter(c, font, size, color) { const style = CharacterStyleFactory.getStyle(font, size, color) const character = new Character(c, style) Editor.chars.push(character) }}如果我們不把樣式提取出來,我們每敲一個文字,都會調(diào)用 Editor 類中的 appendCharacter() 方法,創(chuàng)建一個新的 Character 對象,保存到 chars 數(shù)組中。如果一個文本文件中,有上萬、十幾萬、幾十萬的文字,那我們就要在內(nèi)存中存儲這么多 Character 對象。那有沒有辦法可以節(jié)省一點內(nèi)存呢?

實際上,在一個文本文件中,用到的字體格式不會太多,畢竟不大可能有人把每個文字都設置成不同的格式。所以,對于字體格式,我們可以將它設計成享元,讓不同的文字共享使用。


行為型設計模式
觀察者模式



觀察者模式(Observer Design Pattern)也被稱為發(fā)布訂閱模式(Publish-Subscribe Design Pattern),在對象之間定義一個一對多的依賴,當一個對象狀態(tài)改變的時候,所有依賴的對象都會自動收到通知。




3一般情況下,被依賴的對象叫作被觀察者(Observable),依賴的對象叫作觀察者(Observer)。不過,在實際的項目開發(fā)中,這兩種對象的稱呼是比較靈活的,有各種不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么稱呼,只要應用場景符合剛剛給出的定義,都可以看作觀察者模式。


我們下面實現(xiàn)一個簡單但是最經(jīng)典的實現(xiàn)方式。先通過模板模式的思想把基類寫出來

class Subject { registerObserver() { throw new Error('子類需重寫父類的方法') } removeObserver() { throw new Error('子類需重寫父類的方法') } notifyObservers() { throw new Error('子類需重寫父類的方法') }}class Observer{ update() { throw new Error('子類需重寫父類的方法') }}然后根據(jù)模板來具體的實現(xiàn)

class ConcreteSubject extends Subject { static observers = [] registerObserver(observer) { ConcreteSubject.observers.push(observer) } removeObserver(observer) { ConcreteSubject.observers = ConcreteSubject.observers.filter(item => item !== oberser) } notifyObservers(message) { for(let i = 0,observer; observer = ConcreteSubject.observers[i++];) { observer.update(message) } }}class ConcreteObserverOne extends Observer { update(message) { //TODO 獲得消息通知,執(zhí)行自己的邏輯 console.log(message) //... }}class ConcreteObserverTwo extends Observer { update(message) { //TODO 獲得消息通知,執(zhí)行自己的邏輯 console.log(message) //... }}class Demo { constructor() { const subject = new ConcreteSubject() subject.registerObserver(new ConcreteObserverOne()) subject.registerObserver(new ConcreteObserverTwo()) subject.notifyObservers('copy that') }}const demo = new Demo()事實上,上面只是給出了一個大體的原理和思路,實際中的觀察者模式要復雜的多,比如你要考慮同步阻塞或異步非阻塞的問題,命名空間的問題,還有必須要先注冊再發(fā)布嗎? 而且項目中用了大量的觀察者模式的話會導致增加耦合性,降低內(nèi)聚性,使項目變得難以維護。


模板模式


模板方法模式在一個方法中定義一個算法骨架,并將某些步驟推遲到子類中實現(xiàn)。模板方法模式可以讓子類在不改變算法整體結構的情況下,重新定義算法中的某些步驟。
這里的“算法”,我們可以理解為廣義上的“業(yè)務邏輯”,并不特指數(shù)據(jù)結構和算法中的“算法”。這里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,這也是模板方法模式名字的由來。


比如說我們要做一個咖啡機的程序,可以通過程序設定幫我們做出各種口味的咖啡,它的過程大概是這樣的


有可能有的你不需要加,那我們可以寫一個hook來控制各個變量,比如我們下面給是否加冰加一個hook

class Tea { addCoffee() { console.log('加入咖啡') } addSuger() { throw new Error('子類需重寫父類的方法') } addMilk() { throw new Error('子類需重寫父類的方法') } addIce() { console.log('加入冰塊') } isIce() { return false // 默認不加冰 } addWater() { console.log('加水') } init() { this.addCoffee() this.addSuger() this.addMilk() if (this.isIce()) { this.addIce() } }}模板寫好了,我們接下來做一杯拿鐵

class Latte extends Tea { addSuger() { console.log('加糖') } addMilk() { console.log('加奶') } isIce() { return true }}const ice_latte = new Latte()ice_latte.init()我們可以看到,我們不僅在父類中封裝了子類的算法框架,還將一些不會變化的方法在父類中實現(xiàn),這樣子類就直接繼承就可以了。其實,之前講組合模式的時候,也有用到了模板方法模式,你可以回頭看一眼。


這也是我們講設計原則的時候講到的要基于接口而非實現(xiàn)編程,你可以把他理解為基于抽象而非實現(xiàn)編程。所以開發(fā)前,要做好設計,會讓我們事半功倍。


js去寫模板還是有一丟丟問題的,比如我們在父類中某些方法里面拋出錯誤的方法雖然能實現(xiàn)但是不美觀,如果你熟悉Ts的話,那就很好辦了,你可以去寫一個抽象類,子類去實現(xiàn)這個抽象類,或者規(guī)定接口(interface),父類和子類都基于接口去編程,有興趣的可以自己實現(xiàn)一下。


策略模式


策略模式,英文全稱是 Strategy Design Pattern。定義一族算法類,將每個算法分別封裝起來,讓它們可以互相替換。策略模式可以使算法的變化獨立于使用它們的客戶端(這里的客戶端代指使用算法的代碼)。


我們知道,工廠模式是解耦對象的創(chuàng)建和使用,觀察者模式是解耦觀察者和被觀察者。策略模式跟兩者類似,也能起到解耦的作用,不過,它解耦的是策略的定義、創(chuàng)建、使用這三部分。


策略的定義


// 因為所有的策略類都實現(xiàn)相同的接口,所以我們可以通過模板來定義class Strategy { algorithmInterface() {}}class ConcreteStrategyA extends Strategy { algorithmInterface() { //具體的實現(xiàn) //... }}class ConcreteStrategyB extends Strategy { algorithmInterface() { //具體的實現(xiàn) //... }}//...

策略的創(chuàng)建

因為策略模式會包含一組策略,在使用它們的時候,一般會通過類型(type)來判斷創(chuàng)建哪個策略來使用。為了封裝創(chuàng)建邏輯,我們需要對客戶端代碼屏蔽創(chuàng)建細節(jié)。我們可以把根據(jù) type 創(chuàng)建策略的邏輯抽離出來,放到工廠類中。

class StrategyFactory { strategies = new Map() constructor () { this.strategies.set("A", new ConcreteStrategyA()) this.strategies.set("B", new ConcreteStrategyB()) //... } getStrategy(type) { return type && this.strategies.get(type) } }在實際的項目開發(fā)中,這個模式比較常用。最常見的應用場景是,利用它來避免冗長的 if-else 或 switch 分支判斷。不過,它的作用還不止如此。它也可以像模板模式那樣,提供框架的擴展點等等。


職責鏈模式
職責鏈模式的英文翻譯是 Chain Of Responsibility Design Pattern。將請求的發(fā)送和接收解耦,讓多個接收對象都有機會處理這個請求。將這些接收對象串成一條鏈,并沿著這條鏈傳遞這個請求,直到鏈上的某個接收對象能夠處理它為止。


通俗的講就是在職責鏈模式中,多個處理器(也就是剛剛定義中說的“接收對象”)依次處理同一個請求。一個請求先經(jīng)過 A 處理器處理,然后再把請求傳遞給 B 處理器,B 處理器處理完后再傳遞給 C 處理器,以此類推,形成一個鏈條。鏈條上的每個處理器各自承擔各自的處理職責,所以叫作職責鏈模式。


職責鏈的實現(xiàn)方式有兩種,我們先來看一種理解起來簡單的,HandlerChain 類用數(shù)組來保存所有的處理器,并且需要在 HandlerChain 的 handle() 函數(shù)中,依次調(diào)用每個處理器的 handle() 函數(shù)。


class IHandler { handle() { throw new Error('子類需重寫這個方法') }}class HandlerA extends IHandler { handle() { let handled = false //... return handled }}class HandlerB extends IHandler { handle() { let handled = false //... return handled }}class HandlerChain { handles = [] addHandle(handle) { this.handles.push(handle) } handle() { this.handles.for(let i= 0, handler; handler = this.handles[i++];) { handled = handler.handle() if (handle) { break } } }}const chain = new HandlerChain()chain.addHandler(new HandlerA())chain.addHandler(new HandlerB())chain.handle()第二種方法用到了鏈表的實現(xiàn)方式

class Handler { successor = null setSuccessor(successor) { this.successor = successor } handle() { const isHandle = this.doHandle() if (!isHandle && !!this.successor) { this.successor.handle } } doHandle() { throw new Error('子類需重寫這個方法') }}class HandlerA extends Handler { doHandle() { let handle = false //... return handle }}class HandlerB extends Handler { doHandle() { let handle = false //... return handle }}class HandlerChain { head = null tail = null addHandler(handler) { handler.setSuccessor(null) if (head === null) { head = handler tail = handler return } tail.setSuccessor(handler) tail = handler } handle() { if (!!head) { head.handle() } }}const chain = new HandlerChain()chain.addHandler(new HandlerA())chain.addHandler(new HandlerB())chain.handle()還記得我們之前說裝飾者模式說到的AOP嗎,我們可以把它稍微改造一下,也可以變成職責鏈的方式。

Function.prototype.after = function(afterFn) { let self = this return function() { let res = self.apply(this, arguments) if (res === false) { afterFn.apply(this, arguments) } return ret }}const res = fn.after(fn1).after(fn2).after(fn3)可以看出我們傳進去的afterFn函數(shù)如果返回false的話,會連著這條鏈一直傳下去,直到最后一個,一旦返回true,就不會將請求往后傳遞了。


迭代器模式


迭代器模式,也叫游標模式。它用來遍歷集合對象。這里說的“集合對象”,我們也可以叫“容器”“聚合對象”,實際上就是包含一組對象的對象,比如,數(shù)組、鏈表、樹、圖、跳表。


一個完整的迭代器模式,一般會涉及容器和容器迭代器兩部分內(nèi)容。為了達到基于接口而非實現(xiàn)編程的目的,容器又包含容器接口、容器實現(xiàn)類,迭代器又包含迭代器接口、迭代器實現(xiàn)類。容器中需要定義 iterator() 方法,用來創(chuàng)建迭代器。迭代器接口中需要定義 hasNext()、currentItem()、next() 三個最基本的方法。容器對象通過依賴注入傳遞到迭代器類中。


class ArrayIterator { constructor( arrayList) { this.cursor = 0 this.arrayList = arrayList } hasNext() { return this.cursor !== this.arrayList.length } next() { this.cursor++ } currentItem() { if(this.cursor > this.arrayList.length) { throw new Error('no such ele') } return this.arrayList[this.cursor] } }在上面的代碼實現(xiàn)中,我們需要將待遍歷的容器對象,通過構造函數(shù)傳遞給迭代器類。實際上,為了封裝迭代器的創(chuàng)建細節(jié),我們可以在容器中定義一個 iterator() 方法,來創(chuàng)建對應的迭代器。為了能實現(xiàn)基于接口而非實現(xiàn)編程,我們還需要將這個方法定義在 ArrayList 接口中。具體的代碼實現(xiàn)和使用示例如下所示:



class ArrayList { constructor(arrayList) { this.arrayList = arrayList } iterator() { return new ArrayIterator(this.arrayList) }}const names = ['lee', 'leon','qing','quene']const arr = new ArrayList(names)const iterator = arr.iterator()iterator.hasNext()iterator.currentItem()iterator.next()iterator.currentItem()上面我們只實現(xiàn)了數(shù)組的迭代器,關于對象的迭代器,他們的原理差不多,你可以自己去實現(xiàn)以下。


相對于 for 循環(huán)遍歷,利用迭代器來遍歷有下面三個優(yōu)勢:


狀態(tài)模式


狀態(tài)模式一般用來實現(xiàn)狀態(tài)機,而狀態(tài)機常用在游戲、工作流引擎等系統(tǒng)開發(fā)中。不過,狀態(tài)機的實現(xiàn)方式有多種,除了狀態(tài)模式,比較常用的還有分支邏輯法和查表法。


“超級馬里奧”游戲不知道你玩過沒有?在游戲中,馬里奧可以變身為多種形態(tài),比如小馬里奧(Small Mario)、超級馬里奧(Super Mario)、火焰馬里奧(Fire Mario)、斗篷馬里奧(Cape Mario)等等。在不同的游戲情節(jié)下,各個形態(tài)會互相轉(zhuǎn)化,并相應的增減積分。比如,初始形態(tài)是小馬里奧,吃了蘑菇之后就會變成超級馬里奧,并且增加 100 積分。


實際上,馬里奧形態(tài)的轉(zhuǎn)變就是一個狀態(tài)機。其中,馬里奧的不同形態(tài)就是狀態(tài)機中的“狀態(tài)”,游戲情節(jié)(比如吃了蘑菇)就是狀態(tài)機中的“事件”,加減積分就是狀態(tài)機中的“動作”。比如,吃蘑菇這個事件,會觸發(fā)狀態(tài)的轉(zhuǎn)移:從小馬里奧轉(zhuǎn)移到超級馬里奧,以及觸發(fā)動作的執(zhí)行(增加 100 積分)。


為了方便接下來的講解,我對游戲背景做了簡化,只保留了部分狀態(tài)和事件。簡化之后的狀態(tài)轉(zhuǎn)移如下圖所示:


class MarioStateMachine { constructor() { this.score = 0 this.currentState = new SmallMario(this) } obtainMushRoom() { this.currentState.obtainMushRoom() } obtainCape() { this.currentState.obtainCape() } obtainFireFlower() { this.currentState.obtainFireFlower() } meetMonster() { this.currentState.meetMonster() } getScore() { return this.score } getCurrentState() { return this.currentState } setScore(score) { this.score = score } setCurrentState(currentState) { this.currentState = currentState }}class Mario { getName() {} obtainMushRoom() {} obtainCape(){} obtainFireFlower(){} meetMonster(){}}class SmallMario extends Mario { constructor(stateMachine) { super() this.stateMachine = stateMachine } obtainMushRoom() { this.stateMachine.setCurrentState(new SuperMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 100) } obtainCape() { this.stateMachine.setCurrentState(new CapeMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 200) } obtainFireFlower() { this.stateMachine.setCurrentState(new FireMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 300) } meetMonster() { // do something }}class SuperMario extends Mario { constructor(stateMachine) { super() this.stateMachine = stateMachine } obtainMushRoom() { // do nothing... } obtainCape() { this.stateMachine.setCurrentState(new CapeMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 200) } obtainFireFlower() { this.stateMachine.setCurrentState(new FireMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 300) } meetMonster() { this.stateMachine.setCurrentState(new SmallMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() - 100) }}//CapeMario FireMario格式相同//使用const mario = new MarioStateMachine()mario.obtainMushRoom()mario.getScore()MarioStateMachine 和各個狀態(tài)類之間是雙向依賴關系。MarioStateMachine 依賴各個狀態(tài)類是理所當然的,但是,反過來,各個狀態(tài)類為什么要依賴 MarioStateMachine 呢?這是因為,各個狀態(tài)類需要更新 MarioStateMachine 中的兩個變量,score 和 currentState。


狀態(tài)模式的代碼實現(xiàn)還存在一些問題,比如,狀態(tài)接口中定義了所有的事件函數(shù),這就導致,即便某個狀態(tài)類并不需要支持其中的某個或者某些事件,但也要實現(xiàn)所有的事件函數(shù)。不僅如此,添加一個事件到狀態(tài)接口,所有的狀態(tài)類都要做相應的修改。


中介模式

中介模式定義了一個單獨的(中介)對象,來封裝一組對象之間的交互。將這組對象之間的交互委派給與中介對象交互,來避免對象之間的直接交互。


實際上,中介模式的設計思想跟中間層很像,通過引入中介這個中間層,將一組對象之間的交互關系(或者說依賴關系)從多對多(網(wǎng)狀關系)轉(zhuǎn)換為一對多(星狀關系)。原來一個對象要跟 n 個對象交互,現(xiàn)在只需要跟一個中介對象交互,從而最小化對象之間的交互關系,降低了代碼的復雜度,提高了代碼的可讀性和可維護性。


提到中介模式,有一個比較經(jīng)典的例子不得不說,那就是航空管制。


為了讓飛機在飛行的時候互不干擾,每架飛機都需要知道其他飛機每時每刻的位置,這就需要時刻跟其他飛機通信。飛機通信形成的通信網(wǎng)絡就會無比復雜。這個時候,我們通過引入“塔臺”這樣一個中介,讓每架飛機只跟塔臺來通信,發(fā)送自己的位置給塔臺,由塔臺來負責每架飛機的航線調(diào)度。這樣就大大簡化了通信網(wǎng)絡。


下面我們用代碼實現(xiàn)以下:

class A { constructor() { this.number = 0 } setNumber(num, m) { this.number = num if (m) { m.setB() } }}class B { constructor() { this.number = 0 } setNumber(num, m) { this.number = num if (m) { m.setA() } }}class Mediator { constructor(a, b) { this.a = a this.b = b } setA() { let number = this.b.number this.a.setNumber(number * 10) } setB() { let number = this.a.number this.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)


訪問者模式


允許一個或者多個操作應用到一組對象上,解耦操作和對象本身。
訪問者模式是23中經(jīng)典設計模式中最難理解的設計模式之一,因為它難理解、難實現(xiàn),應用它會導致代碼的可讀性、可維護性變差,所以,訪問者模式在實際的軟件開發(fā)中很少被用到,在沒有特別必要的情況下,建議你不要使用訪問者模式。


而且訪問者模式需要用到函數(shù)的重載,用js去實現(xiàn)是一件費力不討好的事情。為了保證這篇文章的完整性,我們用Java來實現(xiàn)一下,你看看了解一下就可以了。

public interface Visitor { void visit(Engine engine); void visit(Body body); void visit(Car car);}public class PrintCar implements Visitor { public void visit(Engine engine) { System.out.println("Visiting engine"); } public void visit(Body body) { System.out.println("Visiting body"); } public void visit(Car car) { System.out.println("Visiting car"); }}public class CheckCar implements Visitor { public void visit(Engine engine) { System.out.println("Check engine"); } public void visit(Body body) { System.out.println("Check body"); } public void visit(Car car) { System.out.println("Check car"); }}public interface Visitable { void accept(Visitor visitor);}public class Body implements Visitable { @Override public void accept(Visitor visitor) { visitor.visit(this); }}public class Engine implements Visitable { @Override public void accept(Visitor visitor) { visitor.visit(this); }}public class Car { private List<Visitable> visit = new ArrayList<>(); public void addVisit(Visitable visitable) { visit.add(visitable); } public void show(Visitor visitor) { for (Visitable visitable: visit) { visitable.accept(visitor); } }}public class Client { static public void main(String[] args) { Car car = new Car(); car.addVisit(new Body()); car.addVisit(new Engine()); Visitor print = new PrintCar(); car.show(print); }}一般來說,訪問者模式針對的是一組類型不同的對象。不過,盡管這組對象的類型是不同的,但是,它們繼承相同的父類或者實現(xiàn)相同的接口。

在不同的應用場景下,我們需要對這組對象進行一系列不相關的業(yè)務操作(抽取文本、壓縮等),但為了避免不斷添加功能導致類不斷膨脹,職責越來越不單一,以及避免頻繁地添加功能導致的頻繁代碼修改,我們使用訪問者模式,將對象與操作解耦,將這些業(yè)務操作抽離出來,定義在獨立細分的訪問者類中。


備忘錄模式


在不違背封裝原則的前提下,捕獲一個對象的內(nèi)部狀態(tài),并在該對象之外保存這個狀態(tài),以便之后恢復對象為先前的狀態(tài)。
這個模式理解、掌握起來不難,代碼實現(xiàn)比較靈活,應用場景也比較明確和有限,主要是用來防丟失、撤銷、恢復等。


//備忘類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()) //444editor.getContentFromMemento(careTaker.get(1))console.log(editor.getContent()) //333editor.getContentFromMemento(careTaker.get(0))console.log(editor.getContent()) //222備忘錄模式也叫快照模式,具體來說,就是在不違背封裝原則的前提下,捕獲一個對象的內(nèi)部狀態(tài),并在該對象之外保存這個狀態(tài),以便之后恢復對象為先前的狀態(tài)。這個模式的定義表達了兩部分內(nèi)容:一部分是,存儲副本以便后期恢復;另一部分是,要在不違背封裝原則的前提下,進行對象的備份和恢復。


備忘錄模式的應用場景也比較明確和有限,主要是用來防丟失、撤銷、恢復等。它跟平時我們常說的“備份”很相似。兩者的主要區(qū)別在于,備忘錄模式更側(cè)重于代碼的設計和實現(xiàn),備份更側(cè)重架構設計或產(chǎn)品設計。


命令模式


命令模式將請求(命令)封裝為一個對象,這樣可以使用不同的請求參數(shù)化其他對象(將不同請求依賴注入到其他對象),并且能夠支持請求(命令)的排隊執(zhí)行、記錄日志、撤銷等(附加控制)功能。

// 接收者類class Receiver { execute() { console.log('接收者執(zhí)行請求') } } // 命令者class Command { constructor(receiver) { this.receiver = receiver } execute () { console.log('命令'); this.receiver.execute() }}// 觸發(fā)者class Invoker { constructor(command) { this.command = command } invoke() { console.log('開始') this.command.execute() }} // 開發(fā)商const developer = new Receiver(); // 售樓處 const order = new Command(developer); // 買房const client = new Invoker(order); client.invoke()在一些面向?qū)ο蟮恼Z言中,函數(shù)不能當做參數(shù)被傳遞給其他對象,也沒法賦值給變量,借助命令模式,我們將函數(shù)封裝成對象,這樣就可以實現(xiàn)把函數(shù)像對象一樣使用。但是在js中函數(shù)當成參數(shù)被傳遞是再簡單不過的事情了,所以上面的代碼我們也可以直接用函數(shù)來實現(xiàn)。


解釋器模式


解釋器模式為某個語言定義它的語法(或者叫文法)表示,并定義一個解釋器用來處理這個語法。 比如我們要實現(xiàn)一個加減乘除的計算,如果單純用if-else去判斷操作符,這四種運算符號也能夠用,但是如果我們后面要實現(xiàn)一個科學計算的話會需要用到更多的if-else判斷,這未免會有些臃腫


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; } } class MultiplicationExpression { interpret(context) { if (!(context instanceof Context)) { throw new Error("TypeError") } context.sum *= context.sum } } class DivisionExpression { interpret(context) { if (!(context instanceof Context)) { throw new Error("TypeError") } context.sum /= context.sum } } // MultiplicationExpression和DivisionExpression省略 /** 以下是測試代碼 **/ const context = new Context(); // 依次添加: 加法 | 加法 | 減法 表達式 context.add(new PlusExpression()); context.add(new PlusExpression()); context.add(new MinusExpression()); context.add(new MultiplicationExpression()); context.add(new MultiplicationExpression()); context.add(new DivisionExpression()); // 依次執(zhí)行: 加法 | 加法 | 減法 表達式 context.list.forEach(expression => expression.interpret(context)); console.log(context.sum);解釋器模式的代碼實現(xiàn)比較靈活,沒有固定的模板。我們前面說過,應用設計模式主要是應對代碼的復雜性,解釋器模式也不例外。它的代碼實現(xiàn)的核心思想,就是將語法解析的工作拆分到各個小類中,以此來避免大而全的解析類。一般的做法是,將語法規(guī)則拆分一些小的獨立的單元,然后對每個單元進行解析,最終合并為對整個語法規(guī)則的解析。


總結



每個設計模式都應該由兩部分組成:第一部分是應用場景,即這個模式可以解決哪類問題;第二部分是解決方案,即這個模式的設計思路和具體的代碼實現(xiàn)。不過,代碼實現(xiàn)并不是模式必須包含的。如果你單純地只關注解決方案這一部分,甚至只關注代碼實現(xiàn),就會產(chǎn)生大部分模式看起來都很相似的錯覺。


大部分設計模式的原理和實現(xiàn),都非常簡單,難的是掌握應用場景,搞清楚能解決什么問題。


應用設計模式只是方法,最終的目的,也就是初心,是提高代碼的質(zhì)量。具體點說就是,提高代碼的可讀性、可擴展性、可維護性等。所有的設計都是圍繞著這個初心來做的。
所以,在做代碼設計的時候,你一定要先問下自己,為什么要這樣設計,為什么要應用這種設計模式,這樣做是否能真正地提高代碼質(zhì)量,能提高代碼質(zhì)量的哪些方面。如果自己很難講清楚,或者給出的理由都比較牽強,沒有壓倒性的優(yōu)勢,那基本上就可以斷定這是一種過度設計,是為了設計而設計。


實際上,設計原則和思想是心法,設計模式只是招式。掌握心法,以不變應萬變,無招勝有招。所以,設計原則和思想比設計模式更加普適、重要。掌握了設計原則和思想,我們能更清楚地了解為什么要用某種設計模式,就能更恰到好處地應用設計模式,甚至我們還可以自己創(chuàng)造出來新的設計模式。

往期推薦:

「一勞永逸」送你21道高頻JavaScript手寫面試題

一個合格(優(yōu)秀)的前端都應該閱讀這些文章

原文作者:LeonVincent
原文鏈接:https://juejin.im/post/6868054744557060110
原文來源:掘金

關鍵詞:設計,模式,整理

74
73
25
news

版權所有? 億企邦 1997-2025 保留一切法律許可權利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點擊下載Chrome瀏覽器
關閉