当前位置: 首页 > 科技观察

Javascript条件逻辑设计重构

时间:2023-03-20 21:15:28 科技观察

重构方法:分解条件表达式合并条件表达式用guard语句替换嵌套条件表达式用多态替换条件表达式引入特例引入断言极高:性能是两层或三层if嵌套设置更多大功能。可读性降低:我不知道为什么会这样。多个独立的函数,根据每一小段代码的用途,将分解后的新函数命名,并更改原函数中相应的代码来调用新函数,从而更明确地表达你的意图。对于条件逻辑,将每个分支条件分解成一个新的函数带来了额外的好处:它可以突出条件逻辑,更清楚地显示每个分支做了什么,并突出每个分支的原因。这种重构方式其实只是一个提炼函数的应用场景。charge=quantity*plan.regularRate+plan.regularServiceCharge;重构后,将条件提取为独立函数,使用三元运算符重新排列条件语句:charge=summer()?summerCharge():regularCharge();functionsummer(){ return!aDate.isBefore(plan.summerStart)&&!aDate.isAfter(plan.summerEnd);}functionsummerCharge(){ 返回数量*plan.summerRate;}functionregularCharge(){ returnquantity*plan.regularRate+plan.regularServiceCharge;}重构方式二:Mergeconditionalexpression检查条件不同,但最终行为相同,如果发现这种情况,应该使用“逻辑或”和“逻辑与”将它们组合成一个条件表达式。有两个重要的原因用于组合条件代码。首先合并的条件代码会说“其实只有一个条件检查,但是有多个并行的条件需要检查”,这样这个检查的目的就比较明确了。当然,合并前和合并后的代码具有相同的效果,但原始代码的信息是“这里有一些单独的条件测试恰好同时发生”。其次,这种重构通常为细化函数(106)的使用做准备。将检查条件提炼成一个单独的函数对于理清代码的含义非常有用,因为它将描述“做什么”的语句替换为“为什么这样做”。"functiondisabilityAmount(anEmployee){ if((anEmployee.seniority<2)   ||(anEmployee.monthsDisabled>12))return0; if(anEmployee.isPartTime)return0;这个条件表达式使用了一个细化的函数,使用逻辑与和逻辑或:anEmployee.seniority<2)     ||(anEmployee.monthsDisabled>12)     ||(anEmployee.isPartTime));}重构方法三:用guard语句替换嵌套条件表达式条件表达式通常有两种风格,一种其中两个条件分支都是正常行为,第二种风格只有一个分支是正常行为,另一个分支是异常行为。如果两个分支都是正常行为,你应该使用例如ifelse条件表达式;如果条件非常罕见,它应该“应该单独检查条件并返回如果条件为真,则立即从函数中退出。这种单独的检查通常称为“保护声明”。用guard语句替换嵌套条件表达式的本质是对某个分支给予特别关注。如果您使用if-then-else结构,则您将同等重视if分支和else分支。这样的代码结构向读者传达的信息是每个分支都同等重要。警卫声明是不同的。它告诉读者:“这种情况与本函数的核心逻辑无关,如果确实发生,请做一些必要的清理工作并退出。每个函数只能有一个入口和一个出口”是根深蒂固的想法一些程序员。我发现当我使用他们编写的代码时,我经常需要使用guard语句而不是嵌套的条件表达式。今天的编程语言会强制每个函数只有一个入口点。至于“单一退出”的规则,其实用处不大。在我看来,保持代码干净是关键:如果单一出口使函数更具可读性,则使用单一出口;否则,不要。下面例子中的代码可能我们很多人都写过,现在觉得是恶臭代码: if(employee.isSeparated){  result={amount:0,reasonCode:"SEP"}; } else{  if(employee.isRetired){   result={amount:0,reasonCode:"RET"};  }  else{   //计算数量的逻辑   lorem.ipsum(dolor.sitAmet);1   consectetur(adipiscing).elit();   sed.do.eiusmod=tempor.incididunt.ut(labore)&&dolore(magna.aliqua);   ut.enim.ad(minim.veniam);   result=someFinalComputation();  } } 返回结果;想想下层语句的概念,如果用位语句重构很明显:首先,我们可以重写isDead和isSeparated的位语句。functionpayAmount(employee){ letresult; if(employee.isSeparated)return{amount:0,reasonCode:"SEP"}; if(employee.isRetired)return{amount:0,reasonCode:"RET"}; xxxxxxxxx returnsomeFinalComputation();}有时候用位语句不好用,我们可以用条件反转用guard语句替换嵌套的条件表达式。functionadjustedCapital(anInstrument){ letresult=0; if(anInstrument.capital>0){  if(anInstrument.interestRate>0&&anInstrument.duration>0){   result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;  } } returnresult;}位语句着重于处理分支的正常情况的另一种特殊情况,即大多数Don’tgofarelse,所以这次我想表达anInstrument.capital>0带位语句:functionadjustedCapital(anInstrument){ letresult=0; if(anInstrument.capital<=0)returnresult; if(anInstrument.interestRate>0&&anInstrument.duration>0){  result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor; } returnresult;}NextInvertanInstrument.interestRate>0&&anInstrument.duration>0。functionadjustedCapital(anInstrument){ letresult=0; if(anInstrument.capital<=0)返回结果; if(!(anInstrument.interestRate>0&&anInstrument.duration>0))return结果; result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor; returnresult;}!(anInstrument.interestRate>0&&anInstrument.duration>0)看起来不太好,进行更改"anInstrument.interestRate<=0||anInstrument.duration<=0)并使用重构方法2来结合条件表达式。”functionadjustedCapital(anInstrument){ letresult=0; if(anInstrument.capital<=0   ||anInstrument.interestRate<=0   ||anInstrument.duration<=0)返回结果; result=(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor; returnresult;}最常用于去除冗余变量去除result。“函数adjustedCapital(anInstrument){ if(anInstrument.capital <=0   ||anInstrument.interestRate<=0   || return(anInstrument.income/anInstrument.duration)*anInstrument.adjustmentFactor;}重构方法四:用多态代替条件表达式上面介绍的重构方法基本上是实用的条件逻辑本身的结构就足够表达了,但是我想给条件逻辑加上结构。实用类和多态可以更清楚地表达逻辑的拆分。一个常见的场景是:我可以构造一组类型,每个类型处理自己的条件逻辑例如,我会注意到书籍、音乐和食物的处理方式不同,因为它们是不同的类型商品的。最明显的症状是几个函数都有基于类型代码的switch语句。如果是这样,我可以为switch语句中的每个分支逻辑创建一个类,并使用多态来承载ea的独特行为ch类型,从而去除重复的分支逻辑。另一种情况是:有一个基本逻辑,上面有几个变体。基本逻辑可能是最常用或最简单的。我可以把基础逻辑放到一个超类中,这样我可以先理解那部分逻辑,暂时忽略变体,然后我可以把每个变体逻辑单独放到一个子类中,里面的代码强调区别从基本逻辑。多态性一直是解决复杂条件逻辑和改善这种情况的工具。并不是所有的条件逻辑都必须用多态代替,避免滥用。示例:functionplumages(birds){ returnnewMap(birds.map(b=>[b.name,plumage(b)]));}functionspeeds(birds){ returnnewMap(birds).map(b=>[b.name,airSpeedVelocity(b)]));}functionplumage(bird){ switch(bird.type){ case'EuropeanSwallow':  return"平均"; case'AfricanSwallow':  return(bird.numberOfCoconuts>2)?“累”:“平均”; case'NorwegianBlueParrot':  return(bird.voltage>100)?"烧焦":"美丽"; 默认:  返回“未知”; }}函数airSpeedVelocity(鸟){ 开关(bird.type){ 案例'EuropeanSwallow':  返回35; case'AfricanSwallow':  返回40-2*bird.numberOfCoconuts; case'NorwegianBlueParrot':  return(bird.isNailed)?0:10+bird.voltage/10; 默认:  返回null; }}}使用使用进行的标志标志标志,对于对于的的一个个对象对象或者进行判断判断判断判断,有属性判断判断进行进行进行进行进行鸟类的类”发生变化,因此可以创造出对应的类,以多种方式来处理各类独特的行为。functionplumages(birds){ returnnewMap(birds        .map(b=>createBird(b))        .map(bird=>[bird.name,bird.plumage]));}功能速度(鸟){ 返回新地图(鸟        .map(b=>createBird(b))        .map(鸟=>[bird.name,bird.airSpeedVelocity]));}functioncreateBird(bird){ switch(bird.type){ case'EuropeanSwallow':  returnnewEuropeanSwallow(bird); case'AfricanSwallow':  returnnewAfricanSwallow(bird); case'NorwegianBlueParrot':  returnnewNorwegianBlueParrot(bird); 默认:  返回新的鸟(鸟); }}类鸟{ 构造函数(birdObject){  Object.assign(this,birdObject); } getplumage(){  返回“未知”; } getairSpeedVelocity(){  返回空值; }}类EuropeanSwallowextendsBird{ getplumage(){  返回“平均”; } getairSpeedVelocity(){  返回35; }}类AfricanSwallowextendsBird{ getplumage(){  返回(这编号onuts>2)?“累”:“平均”;得到羽毛(){  返回(this.voltage>100)?"烧焦":"美丽";+this.voltage/10; }}重构方法五:引入特例这种方法主要是对一些特殊值在处理之前进行逻辑判断。“数据结构的用户正在检查某个特殊值,并在该特殊值出现时做同样的事情。如果我在我的代码库中发现多个地方以相同的方式处理相同的特殊值,我想收集这个处理逻辑放在一个地方。处理一个好的方法是使用“特例”模式:创建一个特例元素,表达对这种特例的常见行为的处理。这样我就可以替换大部分特例用单个函数调用检查逻辑。特殊情况有几种表现。如果我只需要从这个对象中读取数据,我可以提供一个文字对象(literalobject),其中所有值都被预填充。如果除了简单的值外还需要更多的行为,需要创建一个特殊的对象,其中包含所有常见行为的函数。异常对象可以通过包装类返回,或者通过转换插入到数据结构中。通常重新的值要求一个特例为空,这就是为什么这个模式通常被称为“空对象”模式——一个空对象是一个特例的一个特例。我们先创建一个关于判断特例对象为真的类。此类具有特例为空的所有属性和方法。我们将对即将获取的对象的属性的判断转化为代理值。如果存在,则使用普通对象。如果不存在,则使用我们创建的特殊对象的属性和方法。ClassSite{getcustomer(){returnthis._customer;}}classCustomer{getname(){...}getbillingPlan(){...}setbillingPlan(arg){...}getpaymentHistory(){...}}clientone“constaCustomer=site.customer;//...很多中间代码...letcustomerName;if(aCustomer==="unknown")customerName="occupant";elsecustomerName=aCustomer.name;”clinttwoconstplan=(aCustomer==="unknown")?registry.billingPlans.basic:aCustomer.billingPlan;clintthreeconstweeksDelinquent=isUnknown(aCustomer)?0:aCustomer.paymentHistory.weeksDelinquentInLastYear;改写:classSite{getcustomer(){return(this._customer==="unknown")?createUnknownCustomer():this._customer;}}functioncreateUnknownCustomer(){return{isUnknown:true,name:"occupant",billingPlan:registry.billingPlans.basic,paymentHistory:{weeksDelinquentInLastYear:0,}};}函数isUnknown(arg){返回arg.isUnknown;}constcustomerName=aCustomer.name;constplan=aCustomer.billingPlan;constweeksDelinquent=aCustomer.paymentHistory.weeksDelinquentInLastYear;你必须阅读整个算法才能看到有时程序员会在注释中写下这样的假设,而我要介绍的是一种更好的技术——使用断言来显式标记这些假设。“断言是一个条件表达式,应该永远为真。如果它失败了,程序员就犯了错误。断言的失败不应该在系统的任何地方被捕获。整个程序的行为应该与和完全一致没有断言。”相同的。事实上,某些编程语言中的断言可以在编译时通过开关完全禁用。”人们经常看到人们鼓励使用断言来查找程序中的错误。虽然这是一件好事,但并不是唯一的使用断言的原因。断言是一种有价值的交流形式——它们告诉读者在程序执行的这一点上对程序的当前状态做出了哪些假设。此外,断言对于调试也非常有帮助。另外,因为它们在交流中很有价值,我倾向于留下断言,即使在解决了我目前正在跟踪的错误之后。自测试代码降低了断言的调试价值,因为具有增量近似的单元测试通常有助于更好地调试,但我仍然重视断言的通信值。假设扩张率总是一个整数。applyDiscount(aNumber){return(this.discountRate)?aNumber-(this.discountRate*aNumber):aNumber;}三元到if-else形式。applyDiscount(aNumber){如果(!this.discountRate)返回aNumber;elsereturnaNumber-(this.discountRate*aNumber);}----------------添加断言applyDiscount(aNumber){if(!this.discountRate)returnaNumber;else{assert(this.discountRate>=0);返回一个数字-(this.discountRate*aNumber);}}注意不要滥用断言。我不会使用断言来检查“我认为应该为真”的所有条件,而只会检查“必须为真”的条件。误用断言会导致代码重复,尤其是在处理上述条件逻辑时。所以我发现有必要去除条件逻辑中的重复,通常是借助提炼函数。我只使用断言来防止程序员错误。如果要从某个外部数据源读取数据,那么所有输入值的检查都应该是程序的一等公民,而不是用断言来实现——除非我对这个外部数据源有绝对的信心。断言是帮助我们追踪错误的最后手段,因此,具有讽刺意味的是,我只在我认为它们永远不会失败时才使用它们。