有关系统重构的那些事儿
有关系统重构那些事儿
一、重构是个什么玩意儿东西?
所谓“外事不决问谷歌,内事不决问百度,房事不决问天涯”,重构这个东西,一般还算是内事,所以我先百度了一把,百度百科上面是这么解释重构的:重 构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
听起来比较玄乎,非常专业化。说白了,就一句话:在不改变系统功能的前提下,调整系统内部结构,优化系统性能,让这个系统能够更好的满足客户需求,同时,希望重构完成之后,这个系统能够多蹦跶几年。
二、干嘛要重构?
重构是个什么事儿,前面已经讲得很清楚了。没有让系统变得更漂亮,也没有让用户能够用到的功能有增加,从外在表现层面上看,这就是个戳白子玩意儿啊。有那个时间有那个精力,你不好好做我要的需求,你玩重构?是在拿着我给你的钱不当钱吧?
有吗?天地良心!!!程序员们的脑子里面0和1比较多,还没太多那种浪费老板的钱的龌龊心理。咱们做这个事情,其实是为了省钱!
2.1 为什么我们的系统会不堪重负?
从软件的整个生命周期上面分析,软件太好养了啊!你买个汽车还得隔段时间去做个保养,看看哪个地方要加点机油,刹车片是不是磨损的太厉害了 之类的吧?软件就没有那么多的毛病了,一不会发生物理磨损,二不会因为用的次数多了要换轮胎啥的。但是,为什么软件不是研发出来了之后就能够一直用下去任何问题不出呢?
道理很简单,需求变更、设计不当是两个最大的罪魁祸首。
先说说需求变更的问题:一款软件总是为了解决某个特定的需求而产生的。套用一句很俗的话:时光飞逝,日月如梭。时代是在不断的发展的,客户的业务场景、需求目标人群也是不 断在发生变化的。有些需求相对来说稳定一些,比如说BOSS,套个三户模型就能够吃上很多年饭了;但是有的需求就变化的比较剧烈了,甚至消失,或者转化成 别的需求了,比如说移动互联网视为经典的“永远是BETA版”,各种试错让猿类们有各种砸键盘摔鼠标的想法。然而你能够不跟着变吗?当然,考虑到时间、成 本、运营策略等各类因素,不是所有的需求变化都要在软件系统当中体现。但是你一直以为你的永远是业界最牛屎的,目空一切的话,你会发现摩尔定律会很快在你的身上发挥作用。SO,软件系统必须根据业务场景、运营策略、用户反馈等各方面的信息进行不断的需求变更来满足用户需求,需要进行各种修修补补以保持自己的生命力。
好了,问题就出来了:最开始在设计系统的时候,架构这块不管哪个公司都是会投入大量的精力,内部资深的架构师、工程师一起讨论,什么高内聚、低耦 合,满足未来10-30年的架构可扩展需求都考虑到了。按说这样应该能够避免掉很多问题吧?实际情况还真不是那样。从我入行这些年来看,能够抗过五年不做 大修的系统,架构已经算是非常牛屎的了。为什么会出现这样的情况?第一个,大家看待事情的眼光都是有局限性的,即便是资深的专业人士,也会存在过度设计、设计不足、耦合不合理、可扩展空间留存不足等问题;第二个,随着时间的推移、业务场景的变更,需求是会不断进行调整的。那好,原来已经做好的功能需要修改,原来没有的功能需要新增,原来可能还存在一些藏起来的问题需要修复。加上可能存在的“不近人情的甲方”之类的威逼进度,在实现过程中不可避免的出现了一些违反最初 的设计架构的事情。第三个,人员的变更问题,可能研发这个系统的骨干人员离职了,但是交接不完整或者是交接内容过多导致被交接人一下子无法接受那么多知 识,或者是新来的人员对架构理解不够深入,也同样会导致违反架构设计思想的事情发生;第四个,当然也是最坑爹的:没有文档没有注释。有个哥们笑言程序员最 讨厌的四件事:写注释、写文档、别人不写注释、别人不写文档。文档和注释其实就是个知识的传承,没有这两个东西,违反架构设计思想的事情发生根本不足为奇。本来一扇墙好好的,今天打个洞,明天钻个孔,一段时间之后,就只能用一个成语来形容这个系统了:千疮百孔。BUG越修补发现越多,系统变得越来越难以维护,新的需求越来越难以实现,人员流失也不断增加,对新需求的支持能力越来越差,甚至制约了业务的发展。然后大家把算盘子拿出来噼里啪啦一扒拉,新需求的开发成本还超过开发新系统的成本了,这个时候就是这个软件系统GAME OVER的时候了。
再说说设计的责任。设计上面的问题无非两种,一种叫做过度设计,一种叫做设计不足。
所谓的过度设计,就是代码的灵活性和复杂性超过所需,或者是设计了一些用户根本就用不到的功能。一个原因是沟通不畅,由于在实际的项目开发当中,是每个人负责一块东西,看似分工明确,但是会使每个人都在自己的小天地工作而不关注别人是否已经完成了自己所需,最终导致大量的重复代码。另外个原因是业务理解力不足,或者是需求沟通存在问题,将不需要建父子表的做了父子表,将不需要后台管理维护的东西做成可维护的。用一个词来形容那就是:费力不讨好。
而设计不足产生的原因就海了去了,大部分系统出现问题都是这种问题。总结了一下,主要是以下几个原因:
1) 时间不足,没时间或者抽不出时间,或者时间不允许
2) 知识不足,不知道要怎么做才符合好的软件设计理念
3) 进度压力,程序员被要求在既有系统当中快速的添加新功能
4) 战线过长,程序员被迫同时进行多个项目
长期的设计不足,会使软件开发的节奏变成“快,慢,更慢,不知道怎么下手”,一个可能的演变过程如下:
系统的1.0版本很快就交付了,代码质量谈不上好,但是还好,我们的架构还是可以的,这些问题后面可以慢慢修改和完善;
系统的2.0版本也交付了,在做新需求的时候发现原来有些东西做得真不怎么样,让这次的版本多费了很多功夫;
系统的3.0版本又要交付了,兄弟们,形势比较艰苦,大家咬咬牙,原来的那些功能暂时先不管了,重点保障这次版本的内容,保障关键功能点,千万千万不能出问题。
又来了一堆新需求!!!所有小组长过来开会,评估下要怎么玩儿。。。
2.2 重构能够给我们带来什么好处?
前面也说了,系统重构不是浪费投资人的钱的一个事儿,那在没有引入新功能、满足新需求的情况下,重构能够带来些什么好处?
第一个:持续修正和改进软件设计。
所有优秀的设计都是不断修改而成的,修改意味着重新审视。
应该说,重构和软件设计是一个互相促进的过程。在重构过程中,你会逐渐发现,原来看起来很优秀的设计,其实有可能只是一个合理的解决方案。随着对业 务的熟悉增强,对整个系统的把控增强,设计将更加有针对性,更加合理。如果没有重构的话,程序设计会逐渐逐渐变成一匹脱缰的野马,无法掌控,或者变成一团 看似有序实际无序的乱麻,不知后续如何下手。重构其实就是在整理代码,让各种带着发散倾向、偏离航向倾向的代码,老实起来,本分起来。
第二个:增强代码的可读性。
任何傻瓜都会编写计算机能理解的代码,好的程序员能够编写人能够理解的代码。
一种情况是垃圾代码过多:曾经有个研发小组长跟我鼓吹,他写的一个类达到了3000行之巨,作为新人的我佩服的是五体投地。后面一睹庐山真面目,一 个类里面就一个方法,里面有大约30个业务处理逻辑是相同的,只是其中某个参数的值不一样而已。我很是哭笑不得:你丫就不嫌敲键盘敲得爪子疼吗?结局就是 我花了半个小时,优化成3个方法,加上简单的注释,也不过100来行的代码。另外一种情况是代码晦涩难懂:有些人做事很快,分配给他的任务噼里啪啦很快就 能够搞定,但是代码非常难以阅读。参数命名从a1到a100,方法命名从f1到f10,不是加密胜似加密。像这样的代码,让阅读他代码的人就一个感觉: 晕。
在一个软件的生命周期当中,维护代码的猿类应该以批来计算。但是犹如之前说的注释和文档的情况一样,猿类们在编写代码的时候往往是宽以律己严以律 人。编码的工作不仅仅是写代码那么简单,为了让代码让别人更容易理解,我们需要在进行软件编码的过程中做很多额外的、看似跟编码无关的一些事情。比如说简 明扼要的注释,复杂、独特情况下的备注说明,清晰的排版布局,一看就知道含义的命名。在重构过程中,这些都是需要重点关注的。如果发现代码中的脏乱差问 题,必须要下狠刀子,我砍我砍我砍砍砍!
第三个:抓出代码当中的臭虫
温故而知新。
猿类们应该对这样的情况深有感触:自己写的代码,隔个三五个月,如果不去看注释,会对程序逻辑出现不甚理解的情景。在重构过程中,会逼迫你理解原先所写的代码,思考原来设计的合理性,发现其中的问题和隐患,构建出更优秀的设计和代码。
第四个:提升编程的趣味性、成就感和效率
软件80%的核心功能,通过20%的代码实现。
程序猿是一种很奇怪的动物,他们愿意坐在电脑面前用几十个小时来解决一个问题来享受那么三五分钟的成就感。当你发现一个问题异常复杂的时候,往往不是问题本身造成的,而是你用错了方法,走错了路。拙劣的设计往往导致臃肿的编码和让人转的迷迷糊糊的业务逻辑。改善设计、改进编码风格,都可以有效的提升开发速度。好的设计和代码质量是成功的一半,更是成功的关键因素。在一个有缺陷的设计和混乱代码的基础上的开发,业务表面上进度较快,但是本质上是延后对设计缺陷的发现和错误的修改,也就是延后了开发风险,最终要在开发的后期付出更多的时间和代价。而研发进程中的重构,虽然在当前会减缓速度,但是带来的后发优势却是不可低估的。要知道,项目的维护成本是要远高于开发成本的。
三、什么时候开始重构?
3.1 凡事不过三
应该说,程序猿这个集体是最喜欢重复发明轮子和最讨厌重复劳动的一群人。
写代码这个事情,比较忌讳重复劳动。写一段代码,第一次的话,直接写就行了;第二次,可能皱下眉头,但还是可以继续写,或者翻一下以前的代码COPY过来;第三次还碰到,那说明重构的时机已经成熟了。如果是同一个类当中有相同的代码块,请将它提炼成一个私有方法;如果是不同类当中有相同的代码,请将它提炼成一个公共类中的公共方法。
3.2 看不顺眼
代码写出来了之后,除非出了问题,否则是很少会有人会回头看的。所以,发现代码看不顺眼的情况一般出现在两个场景:新增功能、修改BUG。
修改BUG的时候进行代码重构是比较好理解的,在理顺原来的代码逻辑之后,发现哪个地方有漏洞而引发了问题,顺带着进行一把重构。而为什么新增功能的时候也会引发重构呢?
一般来说,新增功能的重构只会发生在经验丰富的程序员身上。所谓“年轻人只管完成功能,老头子们才考虑性能和效率”的意思就是在这里。老程序员们对业务理解更深入,对代码逻辑处理更为透彻,所以在新增功能的时候,也能够顺带着就把原来的东西进行一番调整。
3.3代码审查
提起这四个字,我感觉这个是我一生的痛。
代码审查做的最好的应该是对日外包的公司,几乎所有出去的代码都会进行审查。尤其是涉及到关键业务逻辑的,甚至是开项目组审查会进行代码审查。但是国内公司做这个的,太少太少,有的公司是有这样的规定,但是流于文字。
其实代码审查应该是整个开发环节当中相当重要的一个环节,这个环节的重要性程度应该说仅次于需求和设计。当然,代码走查是一个对团队成员要求较高的事情,一个是人力要求,团队当中至少要有两个人对于同一个模块比较熟悉,否则的话就发现不了存在的隐患和逻辑错误;第二个是时间要求,要能够做到每天更新的代码每天能够审查。
代码审查主要是要考虑几个事情:第一,建立统一编码规范,统一编码风格。软件研发到了目前这个年代,早就脱离了作坊式、土匪军的做法,玩的都是团队作战、集群攻击了。一个软件公司,如果连统一编码规范都没有,肯定也谈不到统一编码风格了。第二,简化代码逻辑,便于后续员工接手。在代码审查过程中,对于一些复杂逻辑、超长方法、巨大类都能够挑出来,并解决掉,让后续新员工看到的是风格统一、逻辑简单的代码。第三,发现问题,查漏补缺。代码当中的逻辑错误、隐患等在审查的时候就发现出来,以免后面在用户使用过程中报个空指针,抛个错误堆栈什么的。
四、应该怎样进行重构?
4.1 重构的原则
4.1.1 自动化测试优先原则
在进行代码重构的第一步永远都是相同的:为被重构的代码编写一组可靠的自动化测试代码,覆盖原有代码可能出现的任意场景。人在进行测试的时候,可能由于各种各样的原因,会犯一些错误,而机器执行的代码,是避免犯错的好方法。
4.1.2 OCP原则
应该说,OCP(开发-封闭原则)是所有编码人员都需要遵循的一个准则,只有遵循了这个准则,才有可能写出比较优秀的代码。
所谓OCP其实是由两组词语组成的,一个叫做Open For Extension,意思就是我们开发的代码,对于功能的扩展,是比较开放的,这也就意味着一旦我们的系统发生需求变更的时候,我们可以快速的对软件的功能进行扩展,使其满足新的需求;另外一个叫做Close For Modification,意思就是对于软件代码的修改应该是封闭的,也就是说,我们的代码在修改的时候,是不能影响原有功能的使用的。
个人看法,后面这句更加重要,尤其是在进行系统重构的时候。
4.1.3 小步快跑原则
在进行重构的时候,永远都不建议把所有代码都调整完成之后,再进行测试。小步快跑的研发方式其实并不是敏捷开发的专利,而是适用于各类软件开发应用中的一个基础准则。小步快跑的设计思想体现了简单、快速反馈的特点,也更加符合重构应用的场景。我们在优化的时候,就是对系统当中的某个细胞当中存在的病毒进行肢解,换句话说,今天只管今天的精彩,至于明天有什么,我们花个三五分钟想一下就行了,不是今天的重点工作内容。
4.2 重构的重点照顾对象
4.2.1 重复代码
重复代码包括功能性重复和代码完全重复。代码完全重复一般都是程序员们偷懒使用复制粘贴引发的。功能性重复代码则有较多的原因,解决他们的诀窍就在于“提取公因式”(来源于《重构与模式》一书)。
1) 类层次中不同子类存在明显或不明显的重复→形成Template Method
2) 子类中的方法除了对象创建之外其他实现方法都类似→用Factory Method引入多态创建→使用Template Method去除更多重复
3) 构造函数包含重复代码→链构造函数
4) 单独的代码处理一个对象或者一组对象→Composite替换
5) 类层次多个子类都实现了自己的Composite,而且实现可能完全相同→提取Composite
6) 对象处理方法的区别仅在于接口不同→通过Adapter统一接口
7) 条件逻辑处理空对象,而且相同的空逻辑在整个系统中都是重复的→引入Null Object
4.2.2 巨大类或方法
巨大类往往是类抽象不合理的结果,类抽象不合理就降低了代码的复用率。过长的方法由于包含的业务逻辑过于复杂,错误几率将直线上升,代码可读性直线下降,类的健壮性很容易被打破。因此,在看到巨大类或者巨大方法的时候,基本上可以断定这个是存在问题的,需要进行优化处理。
从个人工作经验上面提供一些数字供参考:类的长度不超过3000行,方法的长度不超过120行(差不多就是一个屏幕高,建议值是10-20行)。
可能有的同学会担心性能问题,实际上,这个问题是不存在的。首先,没有三分三,哪敢上梁山?优秀的设计人员都不会对代码进行不成熟的优化;第二,业内已经有人做了性能剖析,发现将许多小方法串起来的性能开销微乎其微,在毫秒级,所以不会成为性能瓶颈;第三,性能上面存在问题,一般都需要通过重构或者改变算法来提升性能,放弃小方法的原则是不大可取的。
4.2.3 逻辑控制过于复杂
逻辑控制复杂是引发代码问题的最重要的原因,也是后续接手代码维护人员最头痛的点。
我们在进行维护型系统的开发的时候,总会碰到一个事情:代码越写越多。好像不这么写,不弄这么多的控制逻辑,这个功能就没办法实现了。但是其实解决问题的方法有很多,做IT的人也都不是傻子,我们完全有能力找到更好、更优秀的方案来做处理。
举个造桥的例子,颐和园的17孔拱桥是非常漂亮的,但是呢,那个桥只能在园子里面用,只能作为景观存在,来艘稍微大点子的船,这个桥就把他拦住了。但是赵州桥呢?整个桥很简单,就一个孔,不影响通航,洪水来了也基本对他没有太大的影响。所以,屹立千年而不倒。这个就是个简单唯美的设计的典型,也是我们学习的对象。
4.2.4 不恰当的暴露
在接口的开放和使用上面,参数、代码、数据的暴露其实是比较讲究的。让客户、第三方开发者看到不太重要或者有间接重要性的代码,会增加系统的风险,同时,会增加接口的复杂度。
一个不恰当暴露的例子:为了研发简单,某个项目团队将全部参数都放到了一个公开的类当中,这样的话,想什么时候用就什么时候用,想要什么参数就有什么参数,想在哪里用就在哪里用。还好这个是做内部管理系统的项目,没有向外部暴露任何接口,知道代码的也就那么几个人,公司里面也没几个人懂程序设计的,所以没有引发灾难。这样做是严重违背了面向对象的原则的,将代码框架变成了高度中央集权制度。而事实上,中央政府是不能替代地方政府做好一切的,地方政府也不可能为了屁大点事情就去找中央政府请示一下。既然是面向对象的编程,那就要遵循属地原则,自己能够解决的事情,不要闹那么大动静。
4.2.5 缺乏必要的注释/注释冗余
前面也说过有关注释和文档的事情,注释的出现主要是要解决两个问题,第一个是说明这段代码是干什么的,第二个是在复杂逻辑、特定逻辑处理、复杂算法时,说明为什么要这么做。
总的来说,中国的程序员们普遍存在的问题就是重视功能实现,轻视代码注释。这个很好理解,毕竟前者更能带来成就感,而后者更加表现为工作量、体力活。有些人甚至鼓吹一点:好的代码是不需要注释的。确实是这样,但是能够写出完全不需要注释的优秀代码的人毕竟还是极品当中的奇葩,一般人还是做不到的。而人的记忆曲线下降的坡度是陡得吓人的,过个两三个月,自己写的代码要去补注释,估计也很难想起当时为什么要这么玩了。所以,必要的注释还是有必要在编码的时候加上的。
同时还要注意注释冗余的问题,这个出现的情况比较少,但是还是存在。以前我们有个规范,注释代码(不含方法说明和类说明)的部分,不能超过整个代码的20%。
关于注释,还必须注意注释的正确性。什么也比不上放置良好的注释来的有用;什么也比不上乱七八糟的注释更有本事搞乱一个模块;什么也不会比陈旧、提供错误信息的注释更有破坏性。