全民資訊網_打造最好的城市地方門戶

建造者模式——幫你解決四大JavaScript問題

2020-03-23 09:06:27 來源:互聯網 閱讀:1073

全文共11238字,預計學習時長33分鐘

圖源:Pexels

在用JavaScript開發APP時,你可能會覺得創建一個復雜對象很困難。一旦這關系到代碼中的某個細節,創建復雜對象就會變得更加復雜,因為它會使APP很占內存。

這樣的困難一般有很多形式。一種是,在試圖創建不同種類的復雜對象時,代碼會變得很冗長;另一種,創建不同對象的過程會被拉長,因為同一等級模塊中的邏輯需要梳理清晰。


這個時候,我們就需要運用到建造者模式(builderpattern)。一些問題運用建造者模式可以得到輕松改善。


首先,什么是建造者模式(builder pattern)?


建造者模式可以將一個復雜的對象的構建與其表示相分離,使得同樣的構建過程可以創建不同的表示。也就是說如果我們用了建造者模式,那么用戶就需要指定需要建造的類型就可以得到它們,而具體建造的過程和細節就不需要知道了。建造者模式實際就是一個指揮者,一個建造者,一個使用指揮者調用具體建造者工作得出結果的客戶。


建造者模式主要用于“分步驟構建一個復雜的對象”,在這其中“分步驟”是一個穩定的算法,而復雜對象的各個部分則經常變化。


通俗的說:就是一個白富美需要建一個別墅,然后直接找包工頭,包工頭再找工人把別墅建好。這其中白富美不用直接一個一個工人的去找。而且包工頭知道白富美的需求,知道哪里可以找到工人,工人可以干活,中間節省了白富美的和工人之間溝通的成本,白富美也不需要知道房子具體怎么建,最后能拿到房就可以了。

圖源:Pexels

今天的文章里,小芯就將和大家一起討論文章開頭提及的問題,以及如何在使用JavaScript的設計過程中解決這些問題。哪些問題可以通過建造者模式得到輕松改善?


首先來看一個不使用建造者模式的例子,再和使用建造者模式的例子進行對比,我們可以看到代碼上的區別。


在下面的代碼示例中,我們試圖定義“frog(青蛙)”這一類。假設,為了讓青蛙完全有能力在野外生存,它們需要兩只眼睛、四條腿、嗅覺、味覺和心跳。


現在,很明顯,在現實世界中,有更多事情牽涉其中,需要某一種氣味才能生存聽起來很荒謬,但我們不需要對每件事都完全實事求是,只要讓它既簡單又有趣就行了。


不用建造者模式


classFrog {

constructor(name, gender, eyes, legs, scent, tongue, heart, weight, height) {

this.name= name

this.gender = gender

this.eyes = eyes

this.legs = legs

this.scent = scent

this.tongue = tongue

this.heart = heart

if (weight) {

this.weight = weight

}

if (height) {

this.height= height

}

}

}

Frog.js hosted with ? by GitHub

使用建造者模式


classFrogBuilder {

constructor(name, gender) {

this.name= name

this.gender = gender

}

setEyes(eyes) {

this.eyes = eyes

returnthis

}

setLegs(legs) {

this.legs = legs

returnthis

}

setScent(scent) {

this.scent = scent

returnthis

}

setTongue(tongue) {

this.tongue = tongue

returnthis

}

setHeart(heart) {

this.heart = heart

returnthis

}

setWeight(weight) {

this.weight = weight

returnthis

}

setHeight(height) {

this.height= height

returnthis

}

}

FrogBuilder.js hosted with ? by GitHub

現在這看起來好像有點矯枉過正了,因為建造者模式下的代碼量更大。


但是如果深入挖掘在開發過程可能發生的所有情況,您將發現,對比這兩個示例,建造者模式下的代碼示例將在促進簡單性、可維護性和提供更多機會,從而在實現強大功能的方面更占優勢。


下面四個是在JavaScript中利用建造者模式設計就可以輕松解決的大問題。


一、可讀性

圖源:Pexels

最近的代碼示例已經變得有點難以閱讀,因為我們必須同時處理多種變化。


如果想創造“青蛙”的實例,就沒有辦法對其置之不理,只能去理解整個過程。


此外,提供一些文檔,否則不能理解為什么tongueWidth被重命名為width。這太荒謬了!


classFrogBuilder {

constructor(name, gender) {

// Ensure that the first character is always capitalized

this.name= name.charAt(0).toUpperCase() + name.slice(1)

this.gender = gender

}

formatEyesCorrectly(eyes) {

returnArray.isArray(eyes) ? { left: eye[0], right: eye[1] } : eyes

}

setEyes(eyes) {

this.eyes =this.formatEyes(eyes)

returnthis

}

setLegs(legs) {

if (!Array.isArray(legs)) {

thrownewError('"legs" is not an array')

}

this.legs = legs

returnthis

}

setScent(scent) {

this.scent = scent

returnthis

}

updateTongueWidthFieldName(tongue) {

constnewTongue= { ...tongue }

delete newTongue['tongueWidth']

newTongue.width= tongue.width

return newTongue

}

setTongue(tongue) {

constisOld='tongueWidth'in tongue

this.tongue = isOld

?this.updateTongueWidthFieldName(tongue, tongue.tongueWidth)

: tongue

returnthis

}

setHeart(heart) {

this.heart = heart

returnthis

}

setWeight(weight) {

if (typeof weight !=='undefined') {

this.weight = weight

}

returnthis

}

setHeight(height) {

if (typeof height !=='undefined') {

this.height= height

}

returnthis

}

build() {

returnnewFrog(

this.name,

this.gender,

this.eyes,

this.legs,

this.scent,

this.tongue,

this.heart,

this.weight,

this.height,

)

}

}

constlarry=newFrogBuilder('larry', 'male')

.setEyes([{ volume:1.1 }, { volume:1.12 }])

.setScent('sweaty socks')

.setHeart({ rate:22 })

.setWeight(6)

.setHeight(3.5)

.setLegs([

{ size:'small' },

{ size:'small' },

{ size:'small' },

{ size:'small' },

])

.setTongue({ tongueWidth:18, color:'dark red', type:'round' })

.build()

FrogBuilder.js hosted with ? by GitHub

從以下四個方面來獲得提升代碼可讀性的能力:


1. 使方法的名稱具有充分的自記錄性


對我們來說,updateTongueWidthFieldName的用處和使用原因很容易被定義。


我們知道這是正在更新字段名。我們也知道其中的原因,因為update這個詞已經意味著更新!這個自記錄的代碼幫助我們假設一個舊的字段名,需要更改以使用新的。


2. 簡短的構造器


完全可以以后再設置別的屬性!


3. 當啟動一個新“frog”時,要清楚了解每個參數


就像讀英語一樣,你需要清楚地設置出“眼睛”“腿”,然后使用建造方法去構建“青蛙”。


4. 將每個邏輯隔離在單獨的容易執行的代碼塊中


當你修改一些東西時,只需要專注于一件事,那就是在代碼塊中被隔離出來的東西。


二、樣板文件(通過模板化解決)

圖源:Unsplash

我們將來可能會遇到的一個問題是,最后會得到一些重復的代碼。


例如,回顧“frog”實例時,你認為當我們想要創造某些特殊類型的青蛙時,它們會具有完全相同的特性嗎?


在現實世界中,青蛙有不同的變種。例如,蟾蜍是青蛙的一種,但并非所有的青蛙都是蟾蜍。所以,這告訴我們蟾蜍有一些與普通青蛙不同的特性。


蟾蜍和青蛙的一個區別是,蟾蜍的大部分時間是在陸地上度過的,而普通青蛙是在水里。此外,蟾蜍的皮膚有干疙瘩,而正常青蛙的皮膚有點黏。


這意味著我們必須以某種方式確保每次青蛙被實例化時,只有一些值可以通過,也有一些值必須通過。


讓我們回到Frog構造器,添加兩個新參數:棲息地和皮膚:


將來可能會遇到的一個問題是,最終會得到一些重復的代碼。


classFrog {

constructor(

name,

gender,

eyes,

legs,

scent,

tongue,

heart,

habitat,

skin,

weight,

height,

) {

this.name= name

this.gender = gender

this.eyes = eyes

this.legs = legs

this.scent = scent

this.tongue = tongue

this.heart = heart

this.habitat = habitat

this.skin = skin

if (weight) {

this.weight = weight

}

if (height) {

this.height= height

}

}

}

Frog.js hosted with ? by GitHub

在兩次簡單的更改后,這個構造器已經有點混亂了!這就是為什么推薦使用建造者模式。


如果把棲息地和皮膚參數放在最后,可能會出現錯誤,因為體重和身高可能很難確定,而這些又都是可變的!


又由于這種可選性,如果用戶不傳遞這些信息,就會出現錯誤的棲息地和皮膚信息。


編輯FrogBuilder來支持棲息地和皮膚:


setHabitat(habitat) {

this.habitat = habitat

}

setSkin(skin) {

this.skin = skin

}

FrogBuilder.js hosted with ? by GitHub

現在假設需要兩只分開的蟾蜍和一只正常的青蛙:


// frog

constsally=newFrogBuilder('sally', 'female')

.setEyes([{ volume:1.1 }, { volume:1.12 }])

.setScent('blueberry')

.setHeart({ rate:12 })

.setWeight(5)

.setHeight(3.1)

.setLegs([

{ size:'small' },

{ size:'small' },

{ size:'small' },

{ size:'small' },

])

.setTongue({ width:12, color:'navy blue', type:'round' })

.setHabitat('water')

.setSkin('oily')

.build()

// toad

constkelly=newFrogBuilder('kelly', 'female')

.setEyes([{ volume:1.1 }, { volume:1.12 }])

.setScent('black ice')

.setHeart({ rate:11 })

.setWeight(5)

.setHeight(3.1)

.setLegs([

{ size:'small' },

{ size:'small' },

{ size:'small' },

{ size:'small' },

])

.setTongue({ width:12.5, color:'olive', type:'round' })

.setHabitat('land')

.setSkin('dry')

.build()

// toad

constmike=newFrogBuilder('mike', 'male')

.setEyes([{ volume:1.1 }, { volume:1.12 }])

.setScent('smelly socks')

.setHeart({ rate:15 })

.setWeight(12)

.setHeight(5.2)

.setLegs([

{ size:'medium' },

{ size:'medium' },

{ size:'medium' },

{ size:'medium' },

])

.setTongue({ width:12.5, color:'olive', type:'round' })

.setHabitat('land')

.setSkin('dry')

.build()

FrogBuilder.js hosted with ? by GitHub

那么,這里的代碼哪里重復了呢?


如果仔細觀察,就會注意到我們必須重復蟾蜍的棲息地和皮膚設置。如果再有五個只屬于蟾蜍的設置呢?那么每次輸出蟾蜍或者是普通青蛙的時候,都要手動操作這個模板。


創建一個模板,按照慣例,通常稱之為指導器(director)。


指導器負責執行創建對象的步驟——通常是在構建最終對象時可以預先定義一些公共結構,比如本例中的蟾蜍。


因此,不必手動設置蟾蜍之間的不同屬性,可以讓指導器直接生成:


classToadBuilder {

constructor(frogBuilder) {

this.builder = frogBuilder

}

createToad() {

returnthis.builder.setHabitat('land').setSkin('dry')

}

}

let mike =newFrogBuilder('mike', 'male')

mike =newToadBuilder(mike)

.setEyes([{ volume:1.1 }, { volume:1.12 }])

.setScent('smelly socks')

.setHeart({ rate:15 })

.setWeight(12)

.setHeight(5.2)

.setLegs([

{ size:'medium' },

{ size:'medium' },

{ size:'medium' },

{ size:'medium' },

])

.setTongue({ width:12.5, color:'olive', type:'round' })

.build()

ToadBuilder.js hosted with ? by GitHub

這樣,就可以避免將蟾蜍的共享樣板文件應用到所有,而只關注其所需屬性。當蟾蜍有更多的獨有屬性時,這將變得更加有用。


三、代碼混亂

圖源:Unsplash

由于粗心大意地開發大型功能代碼塊而導致的錯誤和事故并不少見。此外,當一個代碼塊需要處理太多事情時,指令就很容易被搞錯。


那么,當功能代碼塊(比如構造器)中有太多待處理時,會遇到什么情況?


回到第一個代碼示例(在不用建造者模式的情況下實現),假設必須先添加一些額外的邏輯來接受傳入的參數,然后才能將它們應用于實例:


classFrog {

constructor(name, gender, eyes, legs, scent, tongue, heart, weight, height) {

if (!Array.isArray(legs)) {

thrownewError('Parameter "legs" is not an array')

}

// Ensure that the first character is always capitalized

this.name= name.charAt(0).toUpperCase() + name.slice(1)

this.gender = gender

// We are allowing the caller to pass in an array where the first index is the left eye and the 2nd is the right

// This is for convenience to make it easier for them.

// Or they can just pass in the eyes using the correct format if they want to

// We must transform it into the object format if they chose the array approach

// because some internal API uses this format

this.eyes =Array.isArray(eyes) ? { left: eye[0], right: eye[1] } : eyes

this.legs = legs

this.scent = scent

// Pretending some internal API changed the field name of the frog's tongue from "tongueWidth" to "width"

// Check for old implementation and migrate them to the new field name

constisOld='tongueWidth'in tongue

if (isOld) {

constnewTongue= { ...tongue }

delete newTongue['tongueWidth']

newTongue.width= tongue.width

this.tongue = newTongue

} else {

this.tongue = newTongue

}

this.heart = heart

if (typeof weight !=='undefined') {

this.weight = weight

}

if (typeof height !=='undefined') {

this.height= height

}

}

}

constlarry=newFrog(

'larry',

'male',

[{ volume:1.1 }, { volume:1.12 }],

[{ size:'small' }, { size:'small' }, { size:'small' }, { size:'small' }],

'sweaty socks',

{ tongueWidth:18, color:'dark red', type:'round' },

{ rate:22 },

6,

3.5,

)

Frog.js hosted with ? by GitHub

構造器代碼有點長,因為要處理不同參數,它的邏輯會被弄亂,因此在某些情況下,它甚至不需要很多的邏輯。這可能會讓代碼難懂,特別是在很久沒有看到源代碼的情況下。


如果我們在開發一個frog應用程序,并且想將其實例化,會有一個缺點:必須確保每個得到的參數在遵循函數簽名方面接近100%完美,否則在構建階段會有一些拋出。


如果需要在某個時候仔細檢查“眼睛”的類型,就必須在雜亂的代碼中尋找,才能得到我們要找的。


如果您最終找到了要查找的行,但隨后意識到有另一行代碼正在引用并影響50行之上的同一個參數,您會覺得困擾嗎?


現在你必須回溯一下,才能明白會發生什么。


如果從前面的例子中再看一眼FrogBuilder構造函數,就能夠簡化構造器,使代碼變得不混亂且自然。


四、缺少控制

圖源:Unsplash

最重要的一項是從執行工作的更多控制中感受到好處。


在沒有建造者示例的時候,通過構造器中可以編寫更多的代碼,但嘗試在其中駐留的代碼越多,可讀性就越低,這會使代碼不清楚。


由于我們可以將細節隔離到各自的功能塊中,因此我們在許多方面有了更好的控制。


一種方法是,可以在不添加更多問題的情況下添加驗證,從而使構建階段更加堅實:


setHeart(heart) {

if (typeof heart !=='object') {

thrownewError('heart is not an object')

}

if (!('rate'in heart)) {

thrownewError('rate in heart is undefined')

}

// Assume the caller wants to pass in a callback to receive the current frog's weight and height that he or she has set

// previously so they can calculate the heart object on the fly. Useful for loops of collections

if (typeof heart ==='function') {

this.heart =heart({

weight:this.weight,

height:this.height

})

} else {

this.heart = heart

}

returnthis

}

validate() {

constrequiredFields= ['name', 'gender', 'eyes', 'legs', 'scent', 'tongue', 'heart']

for (let index =0; index < requiredFields.length; index++) {

constfield= requiredFields[index]

// Immediately return false since we are missing a parameter

if (!(field inthis)) {

returnfalse

}

}

returntrue

}

build() {

constisValid=this.validate(this)

if (isValid) {

returnnewFrog(

this.name,

this.gender,

this.eyes,

this.legs,

this.scent,

this.tongue,

this.heart,

this.weight,

this.height,

)

} else {

// just going to log to console

console.error('Parameters are invalid')

}

}

setHeart.js hosted with ? by GitHub

從這個例子可以看出,構建器的每一部分都是在添加驗證或驗證方法后獨立的,以確保在最終構建Frog之前設置好了所有的必需字段。


還可以利用這些開放的機會添加更多自定義輸入數據類型,以構建參數的原始返回值。


例如,添加更多自定義的使用“眼睛”傳遞信息的方式,從而簡化整個過程:


formatEyesCorrectly(eyes) {

// Assume the caller wants to pass in an array where the first index is the left

// eye, and the 2nd is the right

if (Array.isArray(eyes)) {

return {

left: eye[0],

right: eye[1]

}

}

// Assume that the caller wants to use a number to indicate that both eyes have the exact same volume

if (typeof eyes ==='number') {

return {

left: { volume: eyes },

right: { volume: eyes },

}

}

// Assume that the caller might be unsure of what to set the eyes at this current moment, so he expects

// the current instance as arguments to their callback handler so they can calculate the eyes by themselves

if (typeof eyes ==='function') {

returneyes(this)

}

// Assume the caller is passing in the directly formatted object if the code gets here

return eyes

}

setEyes(eyes) {

this.eyes =this.formatEyes(eyes)

returnthis

}

FrogBuilder.js hosted with ? by GitHub

這樣一來,對于來電者來說,輸入什么樣的數據類型就變得更靈活:


// variation 1 (left eye = index 1, right eye = index 2)

larry.setEyes([{ volume:1 }, { volume:1.2 }])

// variation 2 (left eye + right eye = same values)

larry.setEyes(1.1)

// variation 3 (the caller calls the shots on calculating the left and right eyes)

larry.setEyes(function(instance) {

let leftEye, rightEye

let weight, height

if ('weight'in instance) {

weight = instance.weight

}

if ('height'in instance) {

height = instance.height

}

if (weight >10) {

// It's a fat frog. Their eyes are probably humongous!

leftEye = { volume:5 }

rightEye = { volume:5 }

} else {

constvolume= someApi.getVolume(weight, height)

leftEye = { volume }

// Assuming that female frogs have shorter right eyes for some odd reason

rightEye = { volume: instance.gender ==='female'?0.8:1 }

}

return {

left: leftEye,

right: rightEye,

}

})

// variation 4 (caller decides to use the formatted object directly)

larry.setEyes({

left: { volume:1.5 },

right: { volume:1.51 },

})

larry.js hosted with ? by GitHub

以上就是全部內容啦~如果大家還有什么問題,歡迎在評論區暢所欲言喲~

留言點贊關注

我們一起分享AI學習與發展的干貨

如轉載,請后臺留言,遵守轉載規范

推薦閱讀:adobe是什么軟件

让别人下载软件赚钱 青海快3app 排列5走势图500期 股票配资广告 极速快三是不是正规的 秒速快三赛车网址 好运快3计划平台 平安银行股票行情 江苏7位数最新开奖结果 新疆新乐彩11选5 湖北十一选五任五复式投注表