建造者模式——帮你解决四大JavaScript问题

来源:互联网 阅读:- 发布:2020-03-30 19:42:34

全文共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软件有哪些