全文共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學習與發展的乾貨