几天前的一次上线,脑残手抖不小心写了bug,虽然组里的老大没有说什么,但心里面很是难过。同事说我之所以写虫子是因为我讨厌if/else,这个习惯不好。的确,if/else可以帮助我们很方便的写出流程控制代码,简洁明了,这个条件做什么,那个条件做什么,说得很清楚。说真的,我从来不反对if/else,从经验上看,越复杂的业务场景下,代码写的越简单单一,通常越不容易出错。以结果为导向的现代项目管理方式,这是一种很有效实践经验。
同事说的没错,我的确很讨厌if/else。这个习惯很大程度是受Thoughtworks一位咨询师朋友影响,他经常在我耳边唠叨,写代码要干净,要简洁,要灵活多变,不要固守城规,不要动不动就if/else,switch/case。初入it领域,我一直把这句话奉为经典。在以后的学习工作中也时刻提醒自己要让自己的代码尽可能的看起来简洁,不失灵活。不喜欢if/else并不意味着拒绝它,该使用的时候必要使用,比如函数接口入参check,处理异常分支逻辑流程等。通常能不用分支语句,我尽量不会使用,因为我觉得if/else很丑,每每看到if/else代码,总会以挑剔的眼光看待它,想想能不能重构的更好。大多数时候,关于什么好的代码,大家的意见往往分歧很大,每个人都有各自的想法,审查你代码的人可能会选择另一种实现方式,这并不能说明谁对谁错。
OO设计遵循SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)原则,使用这个原则去审视if/else,可能会发现很多问题,比如不符合单一原则,它本身就像一团浆糊,融合了各种作料,黏糊糊的很不干净;比如不符合开闭原则,每新增一种场景,就需要修改源文件增加一条分支语句,业务逻辑复杂些若有1000种场景就得有1000个分支流,这种情况下代码不仅仅恶心问题了,效率上也存在很大问题。由此可见,if/else虽然简单方便,但不恰当的使用会给编码代码带来非常痛苦的体验。针对这种恶心的if/else分支,我们当然首先想到的去重构它--在不改变代码外部功能特征的前提下对代码内部逻辑进行调整和优化,但,如何做呢?前段时间在项目中正好遇到一个恶心的if/else例子,想在这篇博客里和大家分享一下去除if/else重构的历程。
if/else的恶瘤
有句话说的好--好文章是改出来,同样,好的代码也肯定是重构出来的,因为没有哪个软件工程师能够拍着胸脯保证在项目之初代码设计这块,就考虑到了所有需求变化可能性的扩展。随着项目的不断成长,业务逻辑变的越来越复杂,代码也开始变的越来越多,原有的设计可能不再满足需求,那么此时必须要重构。就系统整体架构而言,重构可能需要很大的改动,可能在架构流程上需要评审;就功能内代码层次而言,这种重构在我们编码过程中随时可以进行,类似于if/else,swicth/case这种代码的重构也属于这种类型。今天我们要重构的if/else源码如下所示,针对不同的status code,CountRecoder对象会执行不同的set方法,为不同内部属性赋值。
public CountRecoder getCountRecoder(List countEntries) { CountRecoder countRecoder = new CountRecoder(); for (CountEntry countEntry : countEntries) { if (1 == countEntry.getCode()) { countRecoder.setCountOfFirstStage(countEntry.getCount()); } else if (2 == countEntry.getCode()) { countRecoder.setCountOfSecondStage(countEntry.getCount()); } else if (3 == countEntry.getCode()) { countRecoder.setCountOfThirdtage(countEntry.getCount()); } else if (4 == countEntry.getCode()) { countRecoder.setCountOfForthtage(countEntry.getCount()); } else if (5 == countEntry.getCode()) { countRecoder.setCountOfFirthStage(countEntry.getCount()); } else if (6 == countEntry.getCode()) { countRecoder.setCountOfSixthStage(countEntry.getCount()); } } return countRecoder; }
CountRecoder对象是一个简单的Java Bean,用于保存一天之中六种状态分别对应的数据条目,提供了get和set方法。CountEntry是对应数据库中每种状态的数据条目记录,包含状态code和以及count两个字段, 我们可以使用mybatis实现数据库记录和Java对象之间的转换。上面getCountRecoder的方法实现了将list转换为CountRecoder的功能。
看到这段代码,想必已经有很多人要呵呵了,像一坨啥啥啥,长得这么丑,真不知道它"爸妈"怎么想的,怎么敢"生"出来。啥都不说了,直接回炉重构吧。重构是门艺术,Martin flow曾写过一本书《重构改变代码之道》,里面详细的记录了重构的方法论,感兴趣的朋友可以阅读一下。说到重构,通常我们在重构中会遇到一个问题,那就是如何能够保证重构的代码不改变原有的外部功能特征 ?经过TDD训练的朋友应该知道答案,那就是单元测试,重构之前要写单元测试,准确的来说应该是补单元测试,毕竟TDD的核心理念是测试驱动开发。对于今天博客中分享的例子,因为代码逻辑比较简单,所以偷了懒,省却了单元测试的历程。
重构初体验--反射
要重构上面的代码,对设计模式精通的人可以立马可以看出来这是使用策略模式/状态模式的绝佳场景,将策略模式稍微变换,工厂模式应该也是ok的,当然也有些人会选择使用反射。对于这些方法,这里不一一列出,主要想讲一下使用反射和工厂模式如何解决消除if/else问题,那先说反射吧,代码如下所示:
private static Map methodsMap = new HashMap<>(); static { methodsMap.put(1, "setCountOfFirstStage"); methodsMap.put(2, "setCountOfSecondStage"); methodsMap.put(3, "setCountOfThirdtage"); methodsMap.put(4, "setCountOfForthtage"); methodsMap.put(5, "setCountOfFirthStage"); methodsMap.put(6, "setCountOfSixthStage"); } public CountRecoder getCountRecoderByReflect(List countEntries) { CountRecoder countRecoder = new CountRecoder(); countEntries.stream().forEach(countEntry -> fillCount(countRecoder, countEntry)); return countRecoder; } private void fillCount(CountRecoder shippingOrderCountDto, CountEntry countEntry) { String name = methodsMap.get(countEntry.getCode()); try { Method declaredMethod = CountRecoder.class.getMethod(name, Integer.class); declaredMethod.invoke(shippingOrderCountDto, countEntry.getCount()); } catch (Exception e) { System.out.println(e); } }
重构初体验--所谓模式
使用反射去掉if/else的原理很简单,使用HashMap建立状态码和需要调用的方法的方法名之间的映射关系,对于每个CountEntry,首先取出状态码,然后根据状态码获得相应的要调用方法的方法名,然后使用java的反射机制就可以实现对应方法的调用了。本例中使用反射的确可以帮助我们完美的去掉if/else的身影,但是,众所周知,反射效率很低,在高并发的条件下,反射绝对不是一个良好的选择。除去反射这种方法,能想到的就剩下使用策略模式或者与其类似的状态模式,以及工厂模式了,我们以工厂模式为例,经典的架构UML架构图通常由三个组成要素:
- 抽象产品角色:通常是一个抽象类或者接口,里面定义了抽象方法
- 具体产品角色:具体产品的实现类,继承或是实现抽象策略类,通常由一个或多个组成类组成。
- 工厂角色:持有抽象产品类的引用,负责动态运行时产品的选择和构建
策略模式的架构图和工厂模式非常类似,不过在策略模式里执行的对象不叫产品,叫策略。在本例中,这里的产品是虚拟产品,它是服务类性质的接口或者实现。Ok,按照工厂模式的思路重构我们的代码,我们首先定义一个抽象产品接口FillCountService,里面定义产品的行为方法fillCount,代码如下所示:
public interface FillCountService { void fillCount(CountRecoder countRecoder, int count); }
接着我们需要分别实现这六种服务类型的产品,在每种产品中封装不同的服务算法,具体的代码如下所示:
class FirstStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfFirstStage(count); } } class SecondStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfSecondStage(count); } } class ThirdStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfThirdtage(count); } } class ForthStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfForthtage(count); } } class FirthStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfFirthStage(count); } } class SixthStageService implements FillCountService { @Override public void fillCount(CountRecoder countRecoder, int count) { countRecoder.setCountOfSixthStage(count); } }
紧接着,我们需要是实现工厂角色,在工厂内需要实现产品的动态选择算法,使用HashMap维护状态code和具体产品的对象之间的映射关系,
就可以非常容易的实现这一点,具体代码如下所示:
public class FillCountServieFactory { private static Map fillCountServiceMap = new HashMap<>(); static { fillCountServiceMap.put(1, new FirstStageService()); fillCountServiceMap.put(2, new SecondStageService()); fillCountServiceMap.put(3, new ThirdStageService()); fillCountServiceMap.put(4, new ForthStageService()); fillCountServiceMap.put(5, new FirthStageService()); fillCountServiceMap.put(6, new SixthStageService()); } public static FillCountService getFillCountStrategy(int statusCode) { return fillCountServiceMap.get(statusCode); } }
客户端在具体使用的时候就变的很简单,那getCountRecoder方法就可以用下面的代码实现:
public CountRecoder getCountRecoder(List countEntries) { CountRecoder countRecoder = new CountRecoder(); countEntries.stream().forEach(countEntry -> FillCountServieFactory.getFillCountStrategy(countEntry.getCode()) .fillCount(countRecoder, countEntry.getCount())); return countRecoder; }
重构初体验--Java8对模式设计的精简
和反射一样使用设计模式也同样完美的去除了if/else,但是不得不引入大量的具体服务实现类,同时程序中出现大量的模板代码,使得我们程序看起来很不干净,幸好Java 8之后引入了Functional Interface,我们可以使用lambda表达式来去除这些模板代码。将一个接口变为Functional interface,可以通过在接口上添加FunctionalInterface注解实现,代码如下所示:
@FunctionalInterface public interface FillCountService { void fillCount(CountRecoder countRecoder, int count); }
那么具体的服务实现类就可以使用一个简单的lambda表达式代替,原先的FirstStageService类对象就可以使用下面的表达式代替:
(countRecoder, count) -> countRecoder.setCountOfFirstStage(count)
那么工厂类中的代码就可以变为:
public class FillCountServieFactory { private static Map<Integer, FillCountService> fillCountServiceMap = new HashMap<>(); static { fillCountServiceMap.put(1, (countRecoder, count) -> countRecoder.setCountOfFirstStage(count)); fillCountServiceMap.put(2, (countRecoder, count) -> countRecoder.setCountOfSecondStage(count)); fillCountServiceMap.put(3, (countRecoder, count) -> countRecoder.setCountOfThirdtage(count)); fillCountServiceMap.put(4, (countRecoder, count) -> countRecoder.setCountOfForthtage(count)); fillCountServiceMap.put(5, (countRecoder, count) -> countRecoder.setCountOfFirthStage(count)); fillCountServiceMap.put(6, (countRecoder, count) -> countRecoder.setCountOfSixthStage(count)); } public static FillCountService getFillCountStrategy(int statusCode) { return fillCountServiceMap.get(statusCode); } }
这样我们的代码就重构完毕了,当然了还是有些不完美,程序中的魔法数字不利于阅读理解,可以使用易读的常量标识它们,在这里就不做过多说明了。
总结
Craig Larman曾经说过软件开发最重要的设计工具不是什么技术,而是一颗在设计原则方面训练有素的头脑。重构的最终结果不一定会让代码变少,相反还有可能增加程序的复杂度和抽象性,就本例中的if/else而言,确实如此。我非常赞同我的一位朋友说的话,做技术要有追求,没错if/else可以在代码中工作的挺好,也可以很容易的被接替者所理解,但是我们可以有更好的选择,因为简单的代码也可以变得很精彩。多勤多思,也许有一天真的就可以达到Craig所说的在设计原则方面拥有训练有素的头脑,谁说不是这样呢?加油吧。
相关推荐
《.java代码重构》 代码重构是软件开发过程中的一个重要环节,它涉及到对现有代码的改进,以提高代码的可读性、可维护性,同时并不改变其外在行为。在Java编程中,代码重构是一种常见的实践,尤其在大型项目中,...
Java 代码重构经验分享 Java 代码重构是指在不改变外部行为的情况下,修改代码的内部结构,以提高代码的可维护性、可读性和可扩展性。本文总结了 Java 代码重构的经验和技术规范,包括重构要求、重构的工作、代码的...
### Java代码重构经验总结 在软件开发过程中,代码重构是一项重要的技能,它旨在不改变代码外部行为的前提下,改进其内部结构,从而提升代码质量和可维护性。本文将深入探讨Java代码重构的关键点,涵盖重构原则、...
Java代码重构示例 Java代码重构示例 Java代码重构示例 Java代码重构示例 Java代码重构示例 Java代码重构示例 Java代码重构示例
综上所述,《从Java到Kotlin:重构指南》是一本非常适合Java开发者学习Kotlin并进行代码重构的专业书籍。它不仅提供了丰富的理论知识,还有大量的实践案例供读者参考,是从事软件开发工作的专业人士不可多得的宝贵...
java代码重构以前忽视了,最近在看 字字珠玑,相见恨晚
Java代码重构是一种优化编程实践,旨在改进代码的结构和可读性,而不改变其外部行为。重构对于提高软件质量和维护性至关重要,尤其是在大型项目中。以下是一些在Java重构中的关键原则和技巧,通过实例来展示如何应用...
### Java代码重构:掌握优化现有代码的艺术 #### 引言 在软件开发的过程中,随着项目的不断演进,代码库往往会出现复杂度增加、可读性和可维护性下降的问题。这时,进行代码重构变得至关重要。重构是指在不改变...
Java代码重构是一个重要的软件开发实践,它涉及到对现有代码的改进,目的是提高代码的可读性、可维护性和整体质量,而不改变其外在行为。重构对于大型项目尤其关键,因为随着时间的推移,代码可能会变得复杂且难以...
在某些情况下,开发者可能需要将已有的C++代码转换为Java代码,以便在Java平台上运行或利用Java的生态系统。 标题“C++代码转Java工具”暗示了一个软件或服务的存在,它的功能是自动化C++源代码到Java源代码的转换...
Java代码重构系统1是一个专注于提升代码质量和可维护性的项目,主要目标是对Java代码进行格式、命名和结构的规范化。在代码重构过程中,遵循良好的编码规范是至关重要的,本项目选择了Google的代码规范作为标准。 ...
Martin Fowler所著的《重构:改善既有代码的设计》就是一本专注于Java语言重构实践的经典指南。 本书的核心是向读者传达重构的必要性和重构所能带来的诸多好处。书中详细阐述了重构的基本原理和操作技术,并且用...
以上就是关于Java代码重构、优化以及设计优化的一些关键点,这些知识不仅能提升代码质量,还能帮助开发者更好地应对项目中的各种挑战。通过深入学习《重构-改善既有代码的设计》这样的经典书籍,你可以进一步提升...
标题"**C#代码转Java代码工具**"所暗示的知识点是,存在一种工具或技术能够帮助开发者将C#的源代码转化为等效的Java源代码。这通常是因为项目需求变化、跨平台开发或者对不同语言特性的利用。这种转换工具的工作原理...
改善既有的代码重构(ppt),改善既有的代码重构,改善既有的代码重构PPT
Java设计模式和代码重构是软件开发中的核心概念,它们对于编写高效、可维护的代码至关重要。这个PDF合集涵盖了这两个主题,旨在帮助开发者提升代码质量和软件设计能力。 首先,我们来详细了解一下Java设计模式。...
Java代码重构是软件开发过程中的一个关键环节,它旨在改进代码的结构,提高代码的可读性和可维护性,而不改变其外部行为。这个过程对于长期的项目维护和团队协作至关重要。以下是对“Java代码重构要求简要汇总”文档...
### Java代码重构的策略与技巧 1. **提炼方法**:将一段代码块封装成一个独立的方法,不仅可以减少代码重复,还能使逻辑更加清晰。这是重构中最基础也是最常用的技术之一。 2. **重命名变量和方法**:选择更具有...
《重构改善既有代码的设计》是针对提升Java代码质量的重要参考书籍,它的核心思想在于如何通过重构技术来改善和优化现有的代码设计,使其更为简洁、易于维护和扩展。"重构"一词在软件工程领域指的是在不改变软件外部...
此外,如果Java代码中包含了一些特定于Java平台的API调用,这些部分在Pascal中可能需要替换为相应的函数或库。 总的来说,Java2Pas是一个方便的工具,能够帮助开发者跨越Java和Pascal之间的语言障碍,提高代码复用...