什么是重构
那么,什么才是真正的重构呢?从这本书里可以找到答案。不过,我不打算用书里边的话来说,因为,书中开头部分是没有给出定义的,而是采用了一个经典的小例子加以诠释。而这里,我也不打算直接 copy 作者的例子,而是从我自己的角度来说说什么是重构。
显而易见,重构其实意味着要改变代码,或者说是要重新修改,或者重写代码。当然,他并不是简单的重复工作,这样的重复工作也是没有多大意义的。他的意义就在于,在日常的开发中,通过理解业务需求,捋顺业务逻辑,把繁杂、冗余的代码尽量简化,减少模块之间的耦合。
说到底,真正的重构并不是要等到一个项目的结束,推翻然后重新设计、重新开放。而是在设计之前就要考虑到可能会出现的问题,或者是设计之前借鉴类似的项目。当设计完成之后,转向开发时,编码的过程也是在重构中度过的。开发人员在编码之时,也要考虑到代码的耦合性和功能性。当然,在实际开发中,很多时候是要求进度的,这种情况下很难控制代码的质量,更别说是重构了。其实,这种情况下,是属于快速迭代的产品,每个迭代周期完成之后,还是要有代码评审的,在代码评审时,可以针对具体的代码进行重构,而且,这种方式也有助于知识共享和知识的传播,对于整个团队来说,还是很有必要的。
重构的起点作为开发人员,其担负着把业务需求、功能描述转换成代码的职责,从抽象的需求转换为具体的、直观的实现。那么,所谓的重构的起点在哪里?应该怎么界定是不是需要重构呢?
在《重构》这本书中,就专门提出并解决了这个问题。其中有单独的一章来说明“代码的坏味道”。那么,就来看看都有什么吧,何种情况下,会需要重构呢?
如果你在一个以上的地点看到相同的程序结构,比如说,在一个类中不止一次的用到了同一个方法块,那么可以肯定的是,这个方法(程序结构)已经是重复代码了,你需要设法将它们合而为一,加以提炼。
如果两个毫不相干的类出现重复代码,那么你就要考虑,对其中一个进行简化,将重复代码提炼到一个独立的类中,然后在另一个类中使用这个新类。当然,也可能出现重复代码只应该出现在某个类中,而另外的类则只是引用他。因此,你必须决定这个函数应该放在哪最合适,并确保他被安置后就不会在其他类里出现。
拥有短函数的对象会活的比较好、比较长。换句话说,就是更容易维护。我们经常会说一行代码能完成的事,就别让他写两行。这是因为,函数过于长的话,维护起来会很困难,而且理解起来也是很头疼的事。例如,条件表达式和循环常常也需要重新提炼一翻,把这个体积过于庞大的代码块分割成一个新的方法。
如果想利用单个类做太多的事情,其内往往就会出现太多的实例变量,一段如此,重复代码也就会接踵而至了。就像“单一职责原则”所提倡的那样,每个类只做一件事,这样类的职责单一了,后期的阅读和维护都是很方面的事。对于扩展性来说,也不是什么难事。
记得刚开始学编程的时候,总是把函数所需要的东西以参数的形式传进去,孰不知,参数越传越多,以后再看代码的时候,都不知道这些参数代表什么,难以理解。而学了面向对象编程之后,有了对象,你就不必把函数需要的所有东西都以参数的形式传递给他了,有时,你只需传递一个对象即可解决问题。
设计模式中,有一个原则是“对扩展开放,对修改关闭”。但是,在开发过程中,难免会遇到修改代码的时候,虽然不提倡修改,但我们尽量做到最小的修改,也就是只要修改代码的某一处,就能够解决问题。其实,这就是对上边重复代码进行抽象的结果,只有把重复代码提炼出来,才能做到最小的修改。
这种问题在于,把方法放在了不合适的类中,例如,某个方法在进行计算的时候,会调用另外一个类的很多个方法,那么你就要考虑,这个方法是不是还有必要放在这个类中,是否可以考虑把该方法进行迁移呢?
面向对象程序一个最明显特征就是,少用 Switch 或(case)语句,从本质上说,switch 语句的问题在于重复。你常会发现同样的 switch 语句散布在不同的地点。如果要为他添加一个新的 case 语句,就必须找到所有的 switch 语句并修改他们。而面向对象中多态的出现,则正好解决了这个问题。大多数时候,一看到 switch 语句,你就要考虑用多态来替换他。但如果只是偶尔的一个 switch ,就没有考虑的必要了。
你所创建的每一个类,都得有人去理解他、维护他,这些工作都是要花钱的。如果一个类的所得不值得其身价,它就应该消失。开发中经常会遇到这样的情况,由于业务的变更,或者是由于代码的重构,导致原来的类失去了原有的存在价值,那么,就应该去掉它。
很多时候,我们都喜欢设置临时变量,而起名字的方式也超简便,直接叫一个“tmp”或者“temp”等等,看似简单的背后,却存在着不可忽视的隐患。设想一下,你正在维护一个从来没有接触过的项目,而这个项目的大多数类中,都有很多个临时变量,而且他们的变量名都是“temp”类似的东西,你觉得你能理解代码的意思吗?告诉你,看这样的代码,你会发疯的!
结束语说了这么多,其实仔细想想,这些东西在日常的开发中也都能体会到,只不过有时我们并没有注意过罢了。而且,也有童鞋时不时的会对自己的代码进行优化,使得把大部分的精力都专注在业务开发上,而不是“复制”、“粘贴”重复的代码,因为他们知道,重复的代码只会白白的浪费时间、降低效率。
背景
说到底,重构无非就是为了让代码更加优雅,更加简练,更加高效。重构的结果就是找到一个平衡点,使得项目不仅能够稳定的运行,还能够很容易的被程序员理解,以至维护起来不用那么辛苦。那么,下面就说说重构过程中应该注意哪些事项,以及如何进行重构。
建立测试体系如果你想进行重构,首要前提就是要有一个稳定、可靠的测试环境。作为开发人员,大部分的时间不是消耗在编写代码,而是用来做调试,也就是我们说的测试。一般情况下,修复错误是比较快的,问题就在于,如何能快速的定位错误位置,或者说是如何能快速的找出错误,有时你甚至会花费一整天的时间来排查错误,而且可能还无果而终。
鉴于这样的问题,你就不得不建立自己的一套测试体系,这个测试体系会帮你更快的发现问题,而不至于等到发布测试环境之后,让测试人员再给你提 bug 。当然,有些 bug 是我们测试不出来的,那么为什么不把我们能解决的提前解决掉呢,这样不是更好吗?
至于如何建立测试体系,这个一般是比较规范的公司都会有的。这里我推荐几个单元测试工具,可以基于单元测试工具进行搭建。我自己用的是 jUnit 测试框架,同时你也可以使用 TestNG ,TestNG 也是一款比较出色的测试框架。由于现在的大部分工程都是基于 maven 开发的,也有使用更加先进的 gradle 的,当然也不排除使用 ant 的,这些自动化的构建、部署工具,能够很好的集成测试框架,对于测试来说,大大的方便了开发人员。
重新组织函数我记得《重构》一书中,作者说过,他的重构手法中,大部分是对函数的整理工作,使之更恰当的包装代码。在我看来,其实就是单一职责和消除重复的体现。每个函数只干一件事,函数不要有太多的职责,否则,函数的体积会非常的庞大,不仅对于维护人员来说增加了困难,而且必然会增加重复性、冗余的代码,使得函数越来越臃肿,最终导致崩溃。因此,务必要保持函数的简洁性,始终做一件事即可。
那么,消除重复则是指的,在函数的调用过程中,尽量不直接 copy 所调用函数的实现,而是把多处用到的代码块单独提取出来,抽象成一个新的方法,或者一个新的类,而在用到的地方,直接调用新方法或新类,这样就能有效的避免重复代码导致的恶果。
重新组织对象特性在对象的设计过程中,决定把责任放在哪儿,也是很重要的事。这是因为,类往往会因为承担过多的责任而变得臃肿不堪,导致在调用关系上如同乱麻,没有一个清晰的结构。那么,该如何组织对象的特性呢?
其实,在我们起初设计类的时候,就因为考虑的过少或过多, 导致类的职责并不单一。为了解决职责过多的问题,首先在设计类的时候,就要以单一职责为设计思想,把不相干的函数尽量放到新类中,比如,我要判断一个对象是否为空,那么首先要考虑这个判断为空的方法是不是在别的对象也会用到,如果会的话,那么就要考虑把他迁移到一个新类中,这个新类就是只进行对想验证的工具类。
重新组织对象的特性,其主要的工作还是在于分析类的结构,以及类的职责,弄清楚这个类是做什么的,哪些工作不适合他做,哪些工作是必须要做的,那么重构起来会非常方便的。
简化条件表达式有时候,由于业务逻辑的复杂性,很可能会使用到复杂的条件表达式,甚至要判断的条件会有很多很多,这不仅仅会影响程序的可读性,同时也给维护增加成本,更加不利于扩展等。因此,这种情况下,就要对条件表达式进行简化,那么该如何简化呢?
说到简化条件表达式,说到底还是对业务的耦合,能不能把对业务的耦合抽取出来,是根据这样的业务在项目中存在的多与否,以及有没有相似性,能不能够进行复用。面向对象的封装不就是为了对象的复用嘛。如果某个函数中,if 片段包含太多的话,而这个函数在其他类里没有涉及到,那么,你就可以将 if 段提炼出来,构成一个独立的函数。如果在其他类里有涉及的话,就要优先考虑能否采用某个设计模式,或者利用多态来实现了。
简化函数调用在面向对象技术中,最重要的概念莫过于“接口”了,也有很多开发者经常称是“面向接口编程”。其实,定义一个良好的接口,是开发面向对象软件的关键。那么首要的问题就是,在函数的命名上,要遵守规范的命名方式,我比较喜欢的还是首字母小写的“驼峰式”命名。单词的选择,我个人认为,最好还是选择与所要操作行为意思相同的英文单词进行命名,这样能够很好的理解,毕竟不懂的单词可以查字典嘛。
然后,就是关于接口定义的参数问题了。记得上一篇文章中也说过,参数不能太多,太多的话,不仅影响可读性,在后续的维护中也可能会根本搞不清这些参数都是做什么的。尤其是并发编程的时候,不仅参数名很长,参数的个数也是很多的,而且还要考虑不可修改的问题。其实,完全可以采用不可修改的对象来实现,把参数包装到对象中,给函数传递对象来实现数据的传递。不过,这一块的重构还是需要谨慎一些的。
处理概括关系还有一种关系,在重构中需要特别注意,就是“继承”。在面向对象的设计中,继承给我们提供了很大的便捷性,子类继承父类,子类便可以拥有父类的所有属性和方法(私有的除外)。然而,在重构之时,多个子类中的共同属性,就可以考虑放到父类中,这样即可避免了重复的出现。当然,需要考虑的还有构造函数等。反之,如果父类中的属性或函数在其他子类中是没有用到的,或者说是无用的,而只是针对某一个子类才有的,那么就可以考虑把该属性、函数下移至子类。
结束语说了这么多,也列举了不少方法,但是回过头来想想,重构的最本质的东西还是对事物的抽象、封装和复用。其实,对代码的提炼,就是思想的提炼,就是对面向对象思想的进一步理解。
《重构》带给我的不仅仅是一个修改代码的过程,他还让我体会到代码原来还可以这么优雅,这也就应了那句话,代码也是有生命的,就看你能给她赋予多长的时间。
序
说到重构,大家自然而然会想到很多,不管是怎么开始的,也不管是怎么个重构的过程,到最后都会是两种结果,其一是,重构的很成功,冗余代码得到了简化,代码的可读性、可维护性也有了很大的提高。其二嘛,都能想的到,就是重构失败,这个失败的原因可能有很多。
背景前两篇文章中,分别说明了重构的开始和重构的过程,至于重构的结果嘛,不用我说,大家也都可以想到有两种情况,成功和失败嘛。但是,为什么这里还是要说说结果呢,是因为,对于我来说,我认为,重构的结果固然重要,但是,在重构过程中,你学到的东西要比结果更重要。
感悟今天的这篇文章,我不打算写所谓的正文,但是,我想通过自己的一点感悟来说说对重构的理解。
首先是起点
起点,是当我们闻到代码的“坏味道”,作为一个程序员自我认为的责任感,也可以说是自我修养,把“坏味道”剥离出来,重新设计、抽象、改变为整洁、优秀的代码。至于哪些是所谓的“坏味道”,这个在第一篇文章中已经有所涉及,这里就不再赘述。
我个人认为,找到起点并开始进行重构是很重要的事情,因为很多老的项目中,能够开始重构是鼓起了很大的勇气,除非老项目根本不能支持现在的需求,也会要求重构的。但是,我认为,前者的分险更大一些。
其次是过程
过程,是当我们开始了重构的第一步之后,就要考虑的问题。使用什么样的方法,能够对不同的问题,不同的冗余一 一解决。《重构》一书中也给了很详细的说明,甚至都给出了很多的方案,并给方案进行了命名。从这个过程中,我们尽量剥离业务相关的逻辑,抽象出公用的方法、类,尽量保持代码的唯一性和整洁性。在重构之时,尽量多考虑采用设计模式的方法进行解决。
从重构的过程中,我们可以看到腐败的代码,也可以修改为优秀的代码,然而对比这两者间的不同,就可以发现,好的代码是非常简洁的,多学习优秀的代码,可以使得我们能够提高自身的修养,不仅对开发、编码来说很重要,对于各个模块的设计也是很有必要的。
最后是结果
还是那句话,好的结果是我们所期盼的,如果因为各种原因导致失败,那么最起码在这个过程中,我们学到了很多。
结束语其实,这三篇文章只是我对《重构》这本书的简单的理解,自己也重构了几个小模块,这其中有成功的,也有失败的,在这个过程当中,收获是巨大的,不仅仅在代码上,在设计上,在视野上也更加开阔了,更加认识到自己的渺小。
重构的结束,代表着另一个方向的开始,只有好的设计,基于全局的设计,才能结束重构的局面。
本文作者:紫羽风 来源:CSDN博客
CIO之家 www.ciozj.com 微信公众号:imciow