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

闻起来真香!终于摆脱了那个该死的if-else

时间:2023-03-19 20:05:08 科技观察

ifelse是所有高级编程语言必备的特性。但在现实中,代码中的ifelse往往太多了。图片来自Pexels虽然ifelse是必须的,但是滥用ifelse会对代码的可读性和可维护性造成极大的伤害,进而危及整个软件系统。软件开发领域出现了很多新的技术和概念,但是if...else的基本程序形式并没有太大变化。用好ifelse,不仅对现在很有意义,对以后也很有意义。今天我们就来看看如何把代码中的ifelse“干掉”,让代码耳目一新。问题一:ifelse过多的问题ifelse过多的代码可以抽象为如下代码。只列出了5个逻辑分支,但在实际工作中,你可以看到一个方法包含10个、20个甚至更多的逻辑分支。此外,ifelse过多通常还会伴随另外两个问题:复杂的逻辑表达式和ifelse的深度嵌套。对于后两个问题,本文将在接下来的两节中进行介绍。本节首先讨论ifelse过多的情况。if(condition1){}elseif(condition2){}elseif(condition3){}elseif(condition4){}else{}通常,带有太多ifelse的方法通常不可读和可扩展。从软件设计的角度来看,代码中过多的ifelse往往意味着这段代码违反了单一职责原则和开闭原则。因为在实际项目中,需求是经常变化的,新的需求层出不穷。因此,软件系统的可扩展性非常重要。解决ifelse过多的问题,最大的意义往往在于提高代码的可扩展性。如何解决接下来我们看看如何解决ifelse太多的问题。下面我列举一些解决方案:Table-drivenresponsibilitychainmodeAnnotation-drivenevent-drivenfinitestatemachineOptionalAssertPolymorphicmethod1:Table-drivenfixedlogicexpressionmodeifelse代码可以通过表格的形式表达逻辑表达式一定的映射关系;然后用查表的方法找到某个输入对应的处理函数,用这个处理函数进行计算。适用场景:ifelse固定逻辑表达式模式。实现和示例:if(param.equals(value1)){doAction1(someParams);}elseif(param.equals(value2)){doAction2(someParams);}elseif(param.equals(value3)){doAction3(someParams);}//...可以重构为:Mapaction>actionMappings=newHashMap<>();//这里的泛型是为了方便演示,其实可以换成你需要的类型//WheninitactionMappings.put(value1,(someParams)->{doAction1(someParams)});actionMappings.put(value2,(someParams)->{doAction2(someParams)});actionMappings.put(value3,(someParams)->{doAction3(someParams)});//省略空判断actionMappings.get(param).apply(someParams);上面的例子使用了Java8的Lambda和FunctionalInterface,这里就不做解释了。表的映射关系可以是集中的也可以是分散的,即每个处理类自己注册。也可以用配置文件来表示。简而言之,有多种形式。还有一些问题,条件表达式不像上面的例子那么简单,但稍微改造一下,也可以应用表驱动。下面借用《编程珠玑》的tax计算例子:ifincome<=2200tax=0elseifincome<=2700tax=0.14*(income-2200)elseifincome<=3200tax=70+0.15*(income-2700)elseifincome<=3700tax=145+0.16*(income-3200)......elsetax=53090+0.7*(income-102200)上面的代码,其实只需要提取税金计算公式,提取每个的标准文件转换成表格,只需添加一个循环。具体重构后的代码就不给出了,大家可以自行思考。方法二:责任链模式当ifelse中的条件表达式灵活多变,条件中的数据不能抽象成一张表统一判断时,则应将条件判断权交给每个功能组件。这些部件以链条的形式串联起来,形成一个完整的功能。适用场景:条件表达式灵活多变,没有统一的形式。实现与示例:责任链模式可以在很多开源框架中Filter和Interceptor功能的实现中看到。让我们看看常见的使用模式。重构前:publicvoidhandle(request){if(handlerA.canHandle(request)){handlerA.handleRequest(request);}elseif(handlerB.canHandle(request)){handlerB.handleRequest(request);}elseif(handlerC.canHandle(request)){handlerC.handleRequest(request);}}重构后:}}publicclassHandlerAextendsHandler{publicvoidhandleRequest(Requestrequest){if(canHandle(request))doHandle(request);elseif(next!=null)next.handleRequest(request);}}当然,示例中重构前的代码是为了表达很明显,一些类和方法已经被提取和重构。实际上,它更像是一种平面代码实现。注:责任链的控制模式,责任链模式在具体实施过程中会有一些不同的表现形式。从链式调用控制的角度来看,可以分为外部控制和内部控制两种。外部控制不灵活,但降低了实施难度。责任链中一个环节的具体实现不需要考虑对下一个环节的调用,因为外部统一控制。但是一般的外部控件也不能实现嵌套调用。如果有嵌套调用,又想从外部控制责任链的调用,实现会稍微复杂一些。具体可以参考SpringWebInterceptor机制的实现方法。内部控制更加灵活,具体实现可以决定是否调用链中的下一个环节。但是如果呼叫控制方式是固定的,那么这样的实现对于用户来说是不方便的。设计模式在具体使用中会有很多变体,需要大家灵活掌握。方法三:注解驱动通过Java注解(或其他语言的类似机制)来定义方法执行的条件。程序执行时,通过比较参与注解中定义的条件是否匹配来判断是否调用该方法。具体实现时,可以采用表驱动或责任链的形式实现。适用场景:适用于条件分支较多,对程序扩展性和易用性要求较高的场景。通常是系统中经常遇到新需求的核心功能。实现与示例:这种模式在很多框架中都可以看到,比如常见的SpringMVC。由于这些框架都非常常用,Demo随处可见,这里不再列出具体的demo代码。这种模式的重点是实施。现有的框架用于实现特定领域的功能,例如MVC。因此,如果业务系统采用这种模式,需要自己实现相关的核心功能。主要涉及反射、责任链等技术。具体实现这里就不演示了。方法四:事件驱动通过关联不同的事件类型和相应的处理机制来实现复杂的逻辑,同时达到解耦的目的。适用场景:从理论上讲,事件驱动可以看作是表驱动的一种,但从实际来看,事件驱动与前面提到的表驱动有很多不同。具体来说:表驱动通常是一对一的关系;事件驱动通常是一对多的。在表驱动中,触发和执行通常是强依赖;在事件驱动中,触发和执行是弱依赖。正是以上两者的不同,导致了两者适用场景的不同。具体可以通过事件驱动触发库存、物流、积分等订单支付完成等功能。实现与示例:在实现上,单机练习驱动可以使用Guava、Spring等框架实现。分布式的一般是通过各种消息队列来实现的。但是因为这里主要讨论的是排除ifelse,所以主要针对单机问题域。由于涉及到具体技术,本模式代码不做演示。方法五:有限状态机有限状态机通常称为状态机(无限状态机的概念可以忽略)。首先引用维基百科上的定义:有限状态机(英文:finite-statemachine,缩写:FSM),简称状态机,是一种数学模型,表示有限数量的状态和这些状态之间的转换和动作等行为.其实状态机也可以看作是表驱动的一种,其实就是当前状态和事件的组合以及处理函数之间的一种对应关系。当然,处理成功后会有一个状态转换过程。适用场景:虽然现在互联网后端服务都在强调无状态,但这并不代表不能使用状态机设计。其实在很多场景下,比如协议栈、订单处理等功能,状态机都有其天然的优势。因为在这些场景中有一个自然状态和状态流。实现与示例:要实现状态机设计,首先需要一个相应的框架。该框架至少需要实现一个状态机定义函数,以及为其实现调用路由函数。可以使用DSL或注释来完成状态机定义。原理并不复杂,掌握了注解、反射等功能的同学应该可以轻松实现。参考技术:①ApacheMina状态机ApacheMina框架,虽然在IO框架领域不如Netty,但提供了状态机功能。自己实现过状态机功能的同学可以参考它的源码:https://mina.apache.org/mina-project/userguide/ch14-state-machine/ch14-state-machine.html②SpringStateMachine有很多Spring子项目,包括一个不显眼的状态机框架,可以通过DSL和注解来定义。https://projects.spring.io/spring-statemachine/以上框架仅供参考。如果涉及到具体的项目,需要根据业务特点自行实现状态机的核心功能。方法六:OptionalJava代码中部分ifelse是非空检查导致的。所以,减少这部分带来的ifelse的个数,也可以减少整体ifelse的个数。Java从8开始就引入了Optional类,用来表示可能为空的对象。这个类提供了很多相关操作的方法,可以用来排除ifelse。开源框架Guava和Scala语言提供了类似的功能。使用场景:非空判断有很多ifelse。实现和例子如下,传统写法:Stringstr="HelloWorld!";if(str!=null){System.out.println(str);}else{System.out.println("Null");}使用Optional之后:OptionalstrOptional=Optional.of("HelloWorld!");strOptional.ifPresentOrElse(System.out::println,()->System.out.println("Null"));Optional的方法有很多,这里就不一一介绍了。但是请注意不要使用get()和isPresent()方法,否则和传统的ifelse没什么区别。扩展:KotlinNullSafetyKotlin自带一个叫做NullSafety的特性:bob?.department?.head?.name对于链式调用,在Kotlin语言中你可以传递?以避免空指针异常。如果环为空,则整个链式表达式的计算结果为空。方法七:前面Assert方式的方法适合解决非空检查场景导致的ifelse。类似的场景还有各种参数校验,比如字符串不为空等等。许多框架类库,如Spring和ApacheCommons,都提供了实现这一通用功能的工具。这样,您就不必自己编写ifelse:ApacheCommonsLang中的验证类:https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/验证。htmlSpring的Assert类:https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/Assert.html使用场景:通常用于各种参数校验。扩展:BeanValidation和前面的方法类似,引入了Assert模式,引入了一个效果类似的技术——BeanValidation。BeanValidation是JavaEE规范之一。BeanValidation通过在JavaBeans上使用注解来定义验证标准,然后通过框架统一验证。也可以起到减少ifelse的作用。方法八:多态使用面向对象的多态也可以消除ifelse。这个在代码重构的书上也有介绍:https://refactoring.com/catalog/replaceConditionalWithPolymorphism.html使用场景:链接给出的例子比较简单,不能体现具体的条件适合使用多态来消除ifelse场景。一般来说,当一个类中的多个方法都有类似于例子中的ifelse判断,且条件相同时,可以考虑使用多态来消除ifelse。同时,使用多态并没有完全消除ifelse。相反,ifelse合并被移至对象创建阶段。在创建阶段的if..中,我们可以使用前面介绍的方法。总结:以上部分介绍了ifelse过多导致的问题以及相应的解决方案。除了本节介绍的方法外,还有一些其他方法。比如《重构与模式》一书中就介绍了“用Strategy代替条件逻辑”、“用State代替改变状态的条件语句”和“用Command代替条件调度器”这三种方法。其中,“命令模式”与本文中的“表驱动”方式思路相同。其他两种方法在《重构与模式》一书中都有详细讲解,这里不再赘述。何时使用哪种方法取决于您面临的问题类型。以上介绍的一些适用场景只是一些建议,还需要更多开发者自行思考。问题2:ifelse嵌套很深的问题,就是ifelse太多通常不是最严重的问题。有些代码不仅有大量的ifelse,而且嵌套很深,很复杂,导致代码可读性差,自然难以维护。if(condition1){action1();if(condition2){action2();if(condition3){action3();if(condition4){action4();}}}}ifelse嵌套太深会严重影响代码可读性.当然,还会有上一节提到的两个问题。如何解决上一节介绍的方法也可以用来解决本节的问题,所以本节不再重复上述方法。本节重点介绍一些方法,这些方法不会减少ifelse的数量,但会提高代码的可读性:抽取法Guard语句方法一:抽取法抽取法是代码重构的一种手段。定义很容易理解,就是把一段代码抽取出来放到另外一个单独定义的方法中。适用场景:ifelse重嵌套代码,通常可读性较差。因此,在大规模重构之前,需要做一些小的调整,以提高代码的可读性。提取法是最常用的调整方法。实现和示例如下,重构前:10];for(inti=0;i

猜你喜欢