图源: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学习与发展的干货 如转载,请后台留言,遵守转载规范 |