本文以JavaScript为例,介绍如何优化函数,使其清晰易读,更加高效稳定。软件的复杂性不断增加。代码质量对于保证应用程序的可靠性和可扩展性非常重要。然而,几乎每个开发人员,包括我自己,都在他们的职业生涯中见过低质量的代码。这东西是个坑。低质量的代码有以下几个最致命的特点:函数超长,塞满了各种乱七八糟的函数。函数通常会产生副作用,这些副作用不仅难以理解,而且根本无法调试。模糊的函数和变量命名。脆弱的代码:一个小的改动可能会意外地破坏其他应用程序组件。缺少代码覆盖率。他们基本上听起来像:“我无法理解这段代码是如何工作的”、“这段代码一团糟”、“更改这段代码太难了”等。我有过这样的情况,我的一位同事离开是因为他无法让基于Ruby的RESTAPI更进一步。这个项目是他从之前的开发团队接手的。修复现有错误,引入新错误,添加新功能,添加一系列新错误,等等(所谓的脆弱代码)。客户不想用更好的设计来重构他们的整个应用程序,而开发人员做出明智的选择来维持现状。好吧,它发生了很多,而且很糟糕。那么我们能做些什么呢?首先,重要的是要记住,让应用程序正常工作与让代码质量正常工作是完全不同的事情。一方面,您需要执行产品要求。但另一方面,您应该花时间确保您的函数简单,使用可读的变量和函数名称,避免函数副作用等。函数(包括对象方法)是使应用程序运行的齿轮。首先你应该关注它们的结构和整体布局。这篇文章包含一些非常好的示例,展示了如何编写清晰、易于理解和测试的函数。1.功能要小,非常小。避免使用包含大量函数的大函数,将它们的函数分成几个较小的函数。大型黑盒函数难以理解、修改,尤其难以测试。假设这样一个场景,需要实现一个计算数组、map或普通JavaScript对象权重的函数。总权重可以通过计算每个成员的权重得到:null或undefined变量计1分。基本类型计2分。对象或函数计4分。比如数组[null,'HelloWorld',{}]的权重是这样计算的:1(null)+2(string是原始类型)+4(object)=7。第0步:第一个大函数我们从最坏的实例开始。所有逻辑都编码在函数getCollectionWeight()中:=Object.keys(collection).map(function(key){returncollection[key];});}returncollectionValues.reduce(function(sum,item){if(item==null){returnsum+1;}if(typeofitem==='object'||typeofitem==='function'){returnsum+4;}returnsum+2;},0);}letmyArray=[null,{},15];letmyMap=newMap([['functionKey',function(){}]]);letmyObject={'stringKey':'Helloworld'};getCollectionWeight(myArray);//=>7(1+4+2)getCollectionWeight(myMap);//=>4getCollectionWeight(myObject);//=>2问题很明显,getCollectionWeight()函数超级长,看起来像一个充满“意外”的黑盒子。也许你也发现了,肉眼根本看不出来它在干什么。再想象一下,应用程序中有很多这样的功能。在工作中遇到这样的代码是浪费你的时间和精力。相反,高质量的代码并不让人感到不舒服。在高质量的代码中,那些小的、文档齐全的函数非常容易阅读和理解。第一步:根据类型计算权重,丢弃那些“神秘数字”。现在,我们的目标是将这个巨大的功能拆分成一组更小的、独立的、可重用的功能。第一步是提取根据类型计算权重的代码。这个新函数名为getWeight()。让我们看看这几个“神秘数字”:1、2、4。在不了解整个故事背景的情况下,光靠这几个数字并不能提供任何有用的信息。幸运的是,ES2015允许定义静态只读引用,所以你可以简单地创建几个常量并将那些“神秘数字”替换为有意义的名称。(我特别喜欢术语“神秘数字”:D)让我们创建一个更小的函数getWeightByType()并用它来改进getCollectionWeight()://代码提取到getWeightByType()函数getWeightByType(value){constWEIGHT_NULL_UNDEFINED=1;constWEIGHT_PRIMITIVE=2;constWEIGHT_OBJECT_FUNCTION=4;if(value==null){returnWEIGHT_NULL_UNDEFINED;}if(typeofvalue==='object'||typeofvalue==='function'){returnWEIGHT_OBJECT_FUNCTION;}returnWEIGHT_PRIMITIVE;}functiongetCollectionValeight(collection)if(collectioninstanceofArray){collectionValues=collection;}elseif(collectioninstanceofMap){collectionValues=[...collection.values()];}else{collectionValues=Object.keys(collection).map(function(key){returncollection[key];});}returncollectionValues.reduce(function(sum,item){returnsum+getWeightByType(item);},0);}letmyArray=[null,{},15];letmyMap=newMap([['functionKey',function(){}]]);letmyObject={'stringKey':'Helloworld'};getCollectionWeight(myArray);//=>7(1+4+2)getCollectionWeight(myMap);//=>4getCollectionWeight(myObject);//=>2看起来好多了,对吧?getWeightByType()函数是一个独立的组件,仅用于确定每种类型的权重值,它是可重用的,您可以在任何其他函数中使用它。getCollectionWeight()稍微薄一点。WEIGHT_NULL_UNDEFINED、WEIGHT_PRIMITIVE和WEIGHT_OBJECT_FUNCTION是具有自文档能力的常量,通过名字可以看出每种类型的权重。您无需猜测数字1、2和4的含义。第二步:继续拆分,使其可扩展但是,这个升级版还是有不足之处。假设您计划为一个Set甚至其他用户定义的集合实现权重计算。getCollectionWeight()会很快展开,因为它包含了一组特定的获取权重的逻辑。让我们将获取地图权重的代码提取到getMapValues()中,将获取基本JavaScript对象权重的代码提取到getPlainObjectValues()中。查看改进版本。functiongetWeightByType(value){constWEIGHT_NULL_UNDEFINED=1;constWEIGHT_PRIMITIVE=2;constWEIGHT_OBJECT_FUNCTION=4;if(value==null){returnWEIGHT_NULL_UNDEFINED;}if(typeofvalue==='object'||typeofvalue==='function'){returnWEIGHT_OBJECT_FUNCTION;}returnWEIGHT_PRIMITIVE;}//代码提取到getMapValues()functiongetMapValues(map){return[...map.values()];}//代码提取到getPlainObjectValues()functiongetPlainObjectValues(object){returnObject.keys(object).map(function(key){returnobject[key];});}functiongetCollectionWeight(collection){letcollectionValues;if(collectioninstanceofArray){collectionValues=collection;}elseif(collectioninstanceofMap){collectionValues=getMapValues(collection);}else{collectionValues=getPlainObjectValues(collection);}returncollectionValues.reduce(function(sum,item){returnsum+getWeightByType(item);},0);}letmyArray=[null,{},15];letmyMap=newMap([['functionKey',function(){}]]);letmyObject={'stringKey':'Helloworld'};getCollectionWeight(myArray);//=>7(1+4+2)getCollectionWeight(myMap);//=>4getCollectionWeight(myObject);//=>2现在看getCollectionWeight()函数,你会发现它更容易理解它的机制,它看起来像一个有趣的故事和每个功能的简单性。您无需花时间深入研究代码即可了解代码的工作原理。这就是新版本代码的样子。第3步:优化永无止境即使在这个级别,仍然有很大的优化空间!可以单独创建一个函数getCollectionValues(),使用if/else语句来区分集合中的类型:collection);}然后,getCollectionWeight()应该变得非常纯粹,因为它唯一的工作:使用getCollectionValues()获取值,然后调用求和累加器。您还可以创建一个单独的累加器函数:functionreduceWeightSum(sum,item){returnsum+getWeightByType(item);}理想情况下,getCollectionWeight()函数中不应定义任何函数。***,原来的巨型函数,被改造成了下面这组小函数:除了这些代码质量的优化,你还得到了很多其他的好处:通过代码自文档,getCollectionWeight()函数的可读性性得到极大的改善。getCollectionWeight()函数的长度已大大减少。如果您打算计算其他类型的权重值,getCollectionWeight()的代码将不再显着膨胀。这些拆分功能是低耦合和高度可重用的组件。您的同事可能希望将它们导入到其他项目中,而您可以轻松实现这一需求。当函数偶尔失败时,调用栈会更加详细,因为栈中包含了函数名,甚至可以立马找到错误的函数。这些小功能更简单,更容易测试,并且可以实现很高的代码覆盖率。您可以进行结构化测试并分别测试每个小功能,而不是使用详尽的场景来测试大功能。可以参考CommonJS或者ES2015的模块格式,将split函数创建为一个独立的模块。这将使您的项目文件更轻巧、更有条理。这些技巧可以帮助您克服应用程序的复杂性。原则上,您的函数不应超过20行——越小越好。现在,我想你可能会问我这个问题:“我不想把每一行代码都写成一个函数。有没有什么指导方针告诉我什么时候应该停止拆分?”。那是下一个话题。2.功能要简单让我们放松一下,想一想应用程序的定义是什么?每个应用程序都需要满足一系列要求。开发人员的指导方针是将这些需求拆分成一系列更小的可执行组件(命名空间、类、函数、代码块等),分别完成指定的工作。一个组件由其他更小的组件组??成。如果要编写组件,只能在抽象层中从下层组件中选择需要的组件来创建自己的组件。换句话说,你需要将一个功能分解成几个更小的步骤,并确保这些步骤都在同一抽象层次上,而且只有向下一层。这一点很重要,因为它会让功能变得简单,“做好一件事”。为什么这是必要的?因为简单的功能是很清楚的。清晰意味着易于理解和修改。让我们举个例子。假设您需要实现一个只保留质数数组(2、3、5、7、11等)并删除非质数(1、4、6、8等)的函数。函数调用方法如下:getOnlyPrime([2,3,4,5,6,8,11]);//=>[2,3,5,11]getOnlyPrime()函数如何实现?让我们这样做:为了实现getOnlyPrime()函数,我们使用isPrime()函数来过滤数组中的数字。非常简单,只需对数字数组执行一个过滤函数isPrime()即可。您是否需要在当前抽象级别实现isPrime()的详细信息?不,因为getOnlyPrime()函数在不同的抽象级别实现了一系列步骤。否则,getOnlyPrime()将包含太多功能。带着简单函数的思想,我们来实现getOnlyPrime()函数的主体:functiongetOnlyPrime(numbers){returnnumbers.filter(isPrime);}getOnlyPrime([2,3,4,5,6,8,11]);//=>[2,3,5,11]可以看到,getOnlyPrime()非常简单,它只包含了更底层抽象的步骤:数组的.filter()方法和isPrime()函数。是时候进入下一个抽象级别了。数组的.filter()方法是JavaScript引擎提供的,我们可以直接使用。当然,该标准已经准确描述了它的行为。现在您可以了解isPrime()是如何实现的细节:要实现isPrime()函数来检查数字n是否为质数,只需检查2和Math.sqrt(n)之间的所有整数都不能被n整除.有了这个算法(效率不高,但为了简单起见,让我们使用它),让我们编写isPrime()函数:functionisPrime(number){if(number===3||number===2){returntrue;}if(number===1){returnfalse;}for(letdivisor=2;divisor<=Math.sqrt(number);divisor++){if(number%divisor===0){returnfalse;}}returntrue;}functiongetOnlyPrime(numbers){returnnumbers.filter(isPrime);}getOnlyPrime([2,3,4,5,6,8,11]);//=>[2,3,5,11]getOnlyPrime()小而清晰。它仅从较低的抽象级别采取必要的步骤集。只要遵循这些规则,让函数简洁明了,复杂函数的可读性就会大大提高。代码的精确抽象可以避免代码量大、难维护。3.使用简洁的函数名函数名应该非常简洁:短的和长的。理想情况下,名称应该清楚地概括函数的作用,而不需要读者深入研究函数实现的细节。对于使用驼峰风格的函数名称,以小写字母开头:addItem()、saveToStore()或getFirstName()等。由于函数是某种操作,因此名称中至少应包含一个动词。例如deletePage()、verifyCredentials()。当您需要获取或设置属性时,请使用标准设置和获取前缀:getLastName()或setLastName()。避免在生产代码中使用误导性名称,如foo()、bar()、a()、fun()等。这样的名字没有任何意义。如果函数简短明了,名称简洁:代码读起来像诗。4.总结当然,这里假设的例子都很简单。真正的代码更复杂。您可能会抱怨编写干净的函数并且一次只在抽象上下降一层很无聊。但如果从项目一开始就开始实践,远没有想象的那么复杂。如果你的应用程序中已经有一些复杂的功能,想要重构它们,你可能会觉得很难。在许多情况下,不可能在合理的时间内完成。但千里之行,始于足下:在我们能力的前提下,先拆分一部分。当然,最正确的解决方案应该是从项目一开始就以正确的方式实现应用。除了在实施上花费一些时间之外,还应该花一些精力来构建一个合理的功能结构:正如我们所建议的那样-保持它们简短明了。深谋远虑,笔墨写得有灵气。ES2015实现了非常好的模块体系,这清楚地表明小功能是优秀的工程实践。请记住,干净、组织良好的代码通常需要投入大量时间。你会发现这很难做到。可能需要多次试验,多次迭代和修改一个功能。然而,没有什么比混乱的代码更伤人的了,所以这一切都是值得的!
