精华内容
下载资源
问答
  • 代码腐化
    2013-07-01 20:20:00

    11年刚进入一个新部门,接手一个老项目,典型的legacy code , 一个jsp 好几千行,那叫一个乱。

    但是细细瞧瞧, 还有不少代码是不错的,依稀能看到漂亮代码的影子,可以想象,当初的架构应该还是优美的,只不过经过了若干程序员之手以后,代码慢慢的腐化了。

     

    07 年做的一个项目也是这样,刚开始的时候设计了一个漂亮的架构,大家都严格遵循规则写代码,很注意维护架构的完整性和一致性,也做Code Review,坚决杜绝 dirty code。 随着时间的推移,项目的进度压力加大,什么原则了,纪律了都抛弃了,实现功能是第一要务, 最后系统变成了一个难于理清的大怪物, 现在大家都盼望着它赶紧退休,推倒重写。

     

    联想到我2010年做的咨询项目,客户是行业的领导者,软件和产品运行在世界各地,原以为代码质量会很不错,进入项目组一看,好家伙,代码够乱的,项目组成员在实现新特性的时候,好多copy&paste , 然后就忙着fix bug 。

    我深入的看了它的代码结构, 隐隐约约的看到最初的一些好的原则,模式隐藏在代码的背后,对照现在的代码,让人无限感慨。

     

    代码腐化之路

     

    新项目来了,大家很happy,有机会从头开始构建一个东西,是很难得地,于是仔细小心的设计架构,定下规矩和原则,约定大家都要遵守,刚开始时运转正常,平安无事。

     

    渐渐的出现了一些新情况,需求变动,时间很紧张, 程序员发现有一个非常直接的办法,可以快速的实现客户的要求, 几天就可以搞定, 但是违背了架构的原则或最初的项目的编码约定, 如果想遵循的话,可能需要花费好几倍的工作量,可能需要几周才能完成,更要命的是,为了实现这个新需求,可能需要对整个架构进行调整, 真的调整了,测试跟不上,风险太大, 怎么办?

    大多数情况下,程序员都经不起诱惑,也扛不住进度的压力, 会用最直接的办法进行快速修改,“管他呢,先实现再说,反正我还记得细节”  ,实际上,改完以后我们又忙着干别的事情去了,过上几个月,自己都看不懂了。久而久之,这些脏代码没有人知道是怎么回事了, 后面接手的程序员就会骂前面的程序员 “这么烂的代码,谁写的!!!???” 

     

    代码就是这么腐化的......

    转载于:https://www.cnblogs.com/snake-hand/p/3165571.html

    更多相关内容
  • 写在前面:作者李子昂,阿里巴巴集团研发效能部的第一个算法工程师,目前工作主要方向是代码管理和CI。本文探讨的是:从优化研发交付流程的角度,如何根本上提升研发效能。 先说结论 现在阿里主流的分支开发模式,...

    写在前面:作者李子昂,阿里巴巴集团研发效能部的第一个算法工程师,目前工作主要方向是代码管理和CI。本文探讨的是:从优化研发交付流程的角度,如何根本上提升研发效能。

    先说结论

    现在阿里主流的分支开发模式,以及研发工具中流水线的设计,导致CI和CD流程是捆绑在一起的,生产环节发布成功,自动将分支合入master,也就是说完成一次CD的同时,才进行一次CI。

    这么做也有这么做的好处,你可以保证master上的版本永远和线上代码版本是保持一致的。但是这么做却大大降低了CI的频次,频次越低,体量也就越庞大,单次CI几千行的体量,在阿里内部,倒逼着衍生出了强大的全量回归测试工具,甚至在Code Review中按照依赖链路进行分拆评审的“黑科技”。

    根据混沌理论,改变足够小的时候,结果是可以预测的。我发布一个两行代码的改动,一个单侧就是妥妥覆盖。但是当很多个这样的改动叠加在一起时,结果就变得越来越不可知了,连锁反应可能会造成意料之外的故障。这样一个CI上线的过程中,很多时候测试同学将它当成一个黑盒来测试,全量的去检测所有对外提供的接口,导致测试流程繁重,一旦出现问题,折返路径也相当漫长。

    众所周知,Google是使用大库模式和主干开发的,每一次修改都会很快地合并到master中。高频的CI和直接合入master的高风险,对CI的质量提出了更高的要求,所以诞生了Google的强制CodeReview和重视代码质量的工程师文化。当然这也和Google只要使用C++和Go,和语言特性有关,所以大库+主干在java中的实践也是未来可能会面对的挑战。

    怎么把CI和CD解耦开呢?以制品为分界,解绑CI/CD,释放CI灵活性。在CI流水线的终点,可以把代码的编译产物——制品,存放到制品仓库中。而CD流水线直接选择合适的制品,进行发布。

    在一些自动化测试,扫描和代码分析的服务的辅助下,配合CodeReview,用高频高质量的CI提高代码质量,代码要被review,开发对自己的作品自然会更加有羞耻心和owner意识。用这种研发交付流程的改变去降低代码腐化,鼓励重构。

    这可能是从研发模式上为系统腐化踩刹车的办法。

    研发效能现状

    其实大部分团队的研发效能并没有大多数同学想的那么糟糕。尽管需求交付速度上来看并不快,但只关注单指标,难免管中窥豹。

    人类是唯一可以长时间奔跑的动物。人的身体和耐力非常适合跑步,但是就算是最顶尖的运动员,随着跑步距离的增加,平均时速也会慢慢下降。这种现象,叫做“折中定律”,提升一方面,就要牺牲另一方面。

    在这里插入图片描述

    如果有人让你跑得再快些,你可以答应下来,只要不需要跑很长的距离就好。或者,如果需要长距离选手,那你也可以跑得更远,只要愿意跑慢一些。但你不大可能找到一个既能跑得更快又能跑得更远的人,也没法让一个长跑运动员跑得很快。
    同理,我们不难理解,随着软件复杂度的上升,交付速度的下降也符合折中定律,是自然而然的。
    在这里插入图片描述

    我称之为:“码烂故我慢定律”。

    从另一个角度使用折中定律,也很合理,如果你让我今天就以速度为第一优先,放弃一些代码质量去奔跑,长期来看对平均时速反而是有损害的。
    所以,导致研发速度下降的最主要原因,是软件复杂度的膨胀。谷朴谷子哥有说过:“每新增一行代码对软件系统来说都是负担”。那么如何控制系统复杂度呢?
    图片 代码质量腐化是效能最大的敌人–《Clean Code》书摘
    有些团队在项目初期进展迅速,但一两年之后就慢如蜗行,进度缓慢程度严重。对代码的每次修改都影响到其他两三处代码。修改无小事,每次添加或修改代码,都得对那堆扭纹柴了然于心,这样才能往上扔更多扭纹柴。这团乱麻越来越大,再也无法清理,最后束手无策……这就是混乱的代价:

    随着混乱的增加,团队生产力持续下降,趋向于零。

    花时间保持代码整洁不但有关效率,还有关生存。

    制造混乱无助于赶上期限。混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法——做得快的唯一方法 ——就是始终尽可能保持代码整洁。

    写整洁代码,需要遵循大量的小技巧,贯彻刻苦习得的“整洁感”。这种“代码感”就是关键所在。

    简言之,编写整洁代码的程序员就像是艺术家,他能用一系列变换把一块白板变作由优雅代码构成的系统

    随着混乱的增加,团队生产力持续下降,趋向于零。员工工作成就感也随之降低,离职率上升,让生产力进一步下降。当生产力下降时,管理层就只有一件事可做了:增加更多的人手到项目中,期望提升生产力。

    可是新人并不熟悉系统的设计,他们搞不清楚什么样的设计符合设计意图,什么样的修改违背设计意图。而且他们以及团队中的其他人都背负着提升生产力的可怕压力。于是,他们制造更多的混乱,驱动生产力向零那段不断下降。

    在这里插入图片描述

    在这里插入图片描述

    这些代码是谁写的呢?开发者水平太差么?

    思考问题使很多人习惯于找一个“替罪羊”,但如果这个现象是通病,这很可能是系统的问题,写代码也只是和你一样可能因为对系统不熟悉,或者工期太赶,以快速满足需求为kpi的程序员。

    在这里插入图片描述

    在这里插入图片描述

    系统复杂度的无限熵增,让代码改动变得困难,系统迭代变得缓慢,开发成就感降低,负向飞轮一旦转起来就向无尽的加班和越来越低的效率狂奔。

    系统本身的复杂性是无法避免的,代码本身需要和现实世界交互,而现实世界本身就是复杂的。但复杂的代码也可以是简洁的代码,好的代码质量可以大大降低系统的腐化。

    换句话说,烂代码是研发效能最大的敌人。

    怎么做?提高CI质量,形成正向优化的飞轮
    对于每一位程序员来说,每一次更改代码,都让代码库比你修改前更整洁。

    这句话的思想类似于,勿以善小而不为,勿以恶小而为之。我相信大部分程序员都是有操守的,也愿意护卫代码的整洁。但是如何让这样的努力被看到,让好的代码被看到,让优秀的抽象被认可。

    如何形成重视质量的文化呢?让一线同学认可,让管理者看见一线同学为维护质量作出的努力。

    来看看Google的答卷。大库开发+主干开发模式。大库开发,指所有的代码放在同一个代码库中。主干开发,指开发不能拉分支,只能尽快将自己的修改推送到master分支上。这样的风险显而易见,一旦一个开发把带有Bug的修改push到master上,所有人rebase的代码都是有Bug的,所以对提交代码的代码质量提出了极高的要求。

    Google通过CodeReview文化和强力的CI自动化测试工具支撑了这样的质量体系。每个团队还会有专门的组,一旦有问题的代码被push到了master上,要像处理故障一样回滚和复盘,优化自己的CI质量监控体系。

    仔细想想,这样一套系统自有他的合理性,主干模式开发让代码冲突的问题从低频大问题,变成了高频小问题。这样一次需要CodeReview的代码量也不会过多。同时也避免了代码重复的问题,一是大库代码对大家都可见,二是主干模式更新及时避免了出现并行开发雷同代码的情况。

    对于研发平台方和业务一号位来说,如何设计出这样的研发流程,和能支撑这样研发模式的研发工具,是形成飞轮的关键。

    解绑CICD,释放CI能力
    对于平台方来说:

    提供CI流水线,类似gitlab-runner能力,为研发托管测试devops。让CI与CD解绑,释放CI的频率和能力,通过CodeReview机制,辅以扫描和自动化测试,支撑团队建立适合自己的CI流水线,提升代码质量。

    提供测试托管能力,提供测试集群auto-pilot能力,为CI流水线自动运行测试任务,通过任务托管优化测试资源利用效率。

    对于业务团队来说:
    找到适合自己的CI研发模式,建立质量保证体系。燎原哥的文章对这一块有不错的讨论,我对团队管理经验完全没有,就不展开了。

    Google的最佳实践-大库研发模式和主干研发模式
    并不是说Google的就是好的。但是一个自洽的生态必然是有他的合理性的,就像分支研发模式也有自己的优势,在java场景中,起码在阿里的环境里,分支模式还是有自身的优势的。他山之石,可以攻玉。

    特性分支开发模式
    特性分支开发模式是指为一个或多个特定的需求或者缺陷创建代码分支branch,在其上完成相应的开发后,把它合并(merge)到主干 / 集成分支的开发模式。通常这种分支生命期会持续从几天到几周不等的一段时间。

    在这里插入图片描述

    优点:

    分支互相隔离,可以在宽松的时间内完成对自己分支的测试再合入主干;

    冲突合并次数少,只需要在合并主干的时候进行一次合并;

    缺点:

    分支管理复杂:原因在于大量采用代码分支,且来源分支和合入目标分支各异,操作复杂

    合并冲突多、解决难:分支生命期越长,意味着与主干的代码差异越大,冲突概率越高,冲突的解决难度越大,不论哪种开发模式,其实解决的冲突总数是一定的,分支管理减少了解决冲突的次数,自然需要解决的冲突也就越复杂;

    多测试环境可能会被不同分支抢占,各个分支互相隔离所以代码的测试也需要分开,会对相应的测试环境有抢占的情况;

    主干开发模式
    主干开发,是指开发人员直接向主干分支上推送代码。通常,开发团队的成员 1 天至少 1 次地将代码提交到主干分支。在到达发布条件时,从主干拉出发布分支(通常为 release),用于发布。

    流程:

    在这里插入图片描述

    优点:

    分支模型简单高效,开发人员理解成本低

    分支合并、冲突解决更高频但更容易解决

    随时拥有可发布的版本

    有利于推动高质量的持续集成和持续交付

    缺点:

    合入到主干的代码若质量不过关将直接阻塞整个团队的开发工作,因此需要高质量的CI测试和Code Review工具已经开发文化的支持

    对自动化测试要求高,需有完备单元测试和增量测试能力,确保在代码合入主干前获得快速可靠的质量反馈,大大降低人肉Bug发现和排查的工作量;

    以制品为分界的CI/CD结耦,优化研发交付流程
    CI的流程的最后,代码被编译成制品存入制品仓库,CD流程从制品仓库取制品直接部署。以制品仓库为边界,将CI流程和CD流程结耦开。

    制品可以是C++构建后打binary,也可以是k8s中可以直接部署的容器副本,贯彻GitOps的思想,是阿里目前可见的未来的研发基础设施基石之一。

    目前是阿里巴巴研发效能部在负责这部分工作,云上则有云效团队正在研发同样的云上产品。另外就是可以支撑这样高频高要求CI的CI工具,类似CI流水线。

    聊聊 WorkFlow 流水线

    人靠谱么?我靠谱么?为什么要DevOps,为什么要自动流水线
    我一直搞不懂为什么要搞那么多权限审批。最近突然一堆权限加到了我头上。我发现,自己心情好的时候,会认认真真的盘问申请原因,提醒申请错了表的同学及时更改。但一忙起来,那是真的闭着眼睛过。之所以说这个事,一部分是我在忏悔我对安全不够敬畏,另一方面这也让我深深的意识到一个问题。

    人是系统最大的漏洞。

    众所周知,贝叶斯定律是机器学习的基石,好比穿起羊肉串的那根签。比起笨重的机器,人脑才是最擅长用先验概率来偷懒的鸡贼玩意儿。反正之前那么多次都没出事,似乎顺着老路走总没错。估计在座的读者应该都被「休谟的叉子」叉过。记不起来的话我可以举几个例子提醒一下你:换了宿舍,开学还是顺着肌肉记忆走到了之前的寝室。给手机app位置整理了一遍,刚打开的那几次都得找半天。

    贝叶斯公式没有错。人的大脑也确实很鸡贼。如果没有这种惯性经验,那这日子可过不下去了,要考虑的事情太多。但是经验会犯错,大概率成功并不是100%成功,有的时候环境发生了细微的变化,但是经验却不能敏锐的感知。其次,经验的传递非常困难(各位如果是第二基地的读心术士那当我没说),为什么高的人员流动会严重加速系统腐化和技术债务?因为太多的信息和知识存在了人的经验里,他已经是保持这个系统正常运转的一个部分了,某个小bug经常会触发,他自己知道某个后门可以定时处理订正,但是因为他觉得太低频就没有做成自动化任务。这种玩意儿一旦转手就变成定时炸弹。

    “天猫精灵,放一首《杀死那个石家庄人》”

    WorkFlow 流水线
    还是先围绕着权限申请说吧。人一个个看过去听起来很靠谱,一旦量大了,这件事还靠谱么?反倒是,制定一些规则,比如某些部门的人员可以自动赋权。或者他已经有某些高权限了,在申请相关低权限表可以自动通过。

    可能你会说这个规则也太粗糙了。但是,规则是可以迭代的,可以成长,变得健壮的。而且一旦迭代到一定程度,自动化带来的便捷和安全稳定可以让同学们从重复劳动中解放出来。

    可迭代,自动化,这就是流水线的Key Point。

    Action 组件
    组件是一种即是一种接口也是一种实现,是对底层资源的封装,把复杂的一坨事变成一个好用的按钮。

    以CI/CD流水线为例,比如部署组建,就是把底层的机器资源封装了一层。

    再比如自动化测试组件,替我运行我的测试任务,而我不需要去折腾测试任务的资源和任务的调度,很优雅的设计和封装。

    一个好问题:什么是好的流水线?
    相信大部分读者看到这儿应该和我一样犯病了。

    犯了程序员的职业病,我称这个debuff为「最佳实践缺乏症」,看到啥问题都渴望看到最佳实践。

    可惜我也只是病友,但是有次和燎大师吹逼的时候提过一个观点,我深以为然。

    在这里插入图片描述

    如果我把代码里的一行return true改成return false, 然后我运行CICD流水线,如果它能在发到线上前被拦住,那么这就是一条健壮的流水线。
    这句话很理想,很难实现,但是也很好地点出了可爱的流水线的特质。

    高度自动化,包括优秀的自动化测试能力,越少的人工,系统发挥越稳定。

    可迭代,如果某个问题被系统漏掉了,那么他可以被用来迭代这个系统,直到有一天它不在漏掉这个问题。

    单测,回归测试,不仅是对稳定性的保障,也是对外部系统的一种承诺。不管内部代码如何变换,甚至从零重写了,只要之前的单测和天启之类的回归测试可以通过,那对于外部系统来说就没有任何改变,类似闭包和接口的概念。那么对于应用来说,一条稳定的,测试完整的流水线,是重要的技术资产。

    一个更好的问题
    你可能会想,那我天天就迭代流水线不得累死,问题是搞不完的,万一它真的漏一个bug到线上,算谁的责任呢?似乎迭代出一个完美的,完全自动化的,所有case都考虑到天衣无缝的终态流水线是一个伪命题。

    说到替人做决策,不得不提机器学习。在to B算法产品设计中,有一个原则,我觉得在自动化系统中也适用,套过来可以说:

    自动化系统要做到Human In Loop,它辅助人类完成繁琐的工作,但不是替代人,人可以把精力投入到更有价值的工作中去。

    我相信,在目前的水平来看,就算是最优秀的机器学习算法,最稳定的自动化系统,精准度并不会比行业精英的决策判断更加准确。但是它可以很好的解决效率问题。比如目前的自动驾驶,在安全稳定的情况下提你开车,但是出现紧急情况还是需要人来判断。

    在流水线这件事上,把可以自动化的部分尽量交给自动化工具。在代码设计之类的对复杂场景,使用code review之类的方式,通过人来解决。

    可能你会问,这不还是要人来搞么?但你想想,提一个code review,结果对方让你改一个变量名大小写的错误或者代码规约对问题。再提一次,过了cr结果测试挂了一个,改完有要cr。乒乓review,是不是很痛苦。

    在人类社会提出机器人劳动保护法之前,还是先把这些繁琐的工作交给电脑吧。

    CI WorkFlow 流水线

    说真的流水线真不是什么新鲜玩意儿了。阿里Aone的流水线相信各位都是低头不见抬头见了,大部分公司都有成熟的流水线用来发布。但是最近gitlab action又又又火了。

    Github action
    Action是单一的动作可以执行某些功能,比如部署action,代码合并action之类的,把动作组合编排起来则能完成一个有意义的事情,即Workflow

    在这里插入图片描述

    Action类型分为两大类:

    1. Github上的代码库,这里又可以分为3种情况:
    • Github pre-built action:Github官方创建的action,主要分为3种类型,一种是运行action需要的组件,如bin,hcl等,另一类是公共云集成,如aws,gcloud,azure,还有一类是例子,以example-为前缀。

    • Marketpace:action市场,这里才是Github的厉害之处,以强大的Github生态构建的市场,将会出现如雨后春笋般的action被创建出来。五花八门,可以满足各种各样的需求。

    • Custom Action:如果上述的action不能满足你的需求时,还可以自定义地创建新的action,这里的action可以是私有库的,也可以是公开库的。一个公开库里面的action如果对其他人有用时,还可以发布到市场里面去。

    1. Docker Image,支持两种来源:
    • Docker Hub

    • 任何公开的Docker Image库

    CI何尝不能是一条流水线
    圣诞节收到一个锤子做礼物的小孩,会觉得家里所有的东西都需要敲一敲。

    相信各位看完这个github action的介绍,就像拿到了锤子的小孩,想对着自己的日常工作来两下子,让自己过的轻松一些。

    来设想一下CI流水线可以有什么能力?

    最最基础的,我们团队的CI流程可以被定义了,而且不需要口口相传了。新来的实习生写完了代码,只需要懂怎么触发流水线就行了,不用知道自己要先测那先部分,是发布前给师兄cr还是写完了就给他看?

    测试可以左移到CI流程了,这部分可能就依赖Action生态的优秀程度了。但是这种插件化的机制就很有想象力,再也不用等Aone代码组那几个老哥挤牙膏了,开源的扫描任务或者测试任务用action的方式就能接入。尤其是云原生时代有了IaC之后,也不需要像现在的Aone实验室一样还得有专门的镜像才能跑对应的哪几种任务了。外面的Action也可以搞进来用。

    离梦到来还有多远?

    明显能看到几个技术前置依赖项待解决:

    文件级别的权限管控,现在啥啥都as code了,都往代码里存,这玩意儿可不是谁都能改啊,安全小哥震怒!把关键配置文件,只允许部分同学改动编辑的能力是很有必要的。

    强大简约的WorkFlow框架能力。能云原生化,serverless化,甚至faas化托管action的流水线。看了网上很多对github action的评测文章,发现有的拿来给一些网站做签到脚本,虽然说其实也是在用流水线,我倒觉得更像是在用它的Faas能力。对于测试来说,faas化的测试和扫描,以后一个新的扫描或者测试任务不在需要维护一个单独的集群或者部署一整套应用,而是只要写好业务逻辑,把任务调度交给workflow系统。

    优秀实用的一些关键的基础Action组建,比如优秀的Code Review能力(CI流水线基础中的基础),比如制品仓库的能力(可以将CI和CD流水线结偶的关键),比如自动化测试的组建,以及最最关键的,发布和git操作的能力。

    可能的问题

    Q:干净的代码也只是降低系统的腐化,要怎么逆转效能的降低,做熵减呢?

    A:设计是在信息不完全的情况下做出的决策,所以早期对领域模型的抽象肯定会有不合理性。更好的抽象和重构可以让应用重回青春。

    Q:一定要用主干模式替换分支模式么?

    A:分支模式也有分支模式的好处,这里可以再用一次折中定律,提升一方面,就要牺牲另一方面。所以什么开发模式其实只是手段,更关键的是如何提升代码质量,建立保证代码质量对研发体系,和形成肯定代码质量的文化。(完)
    重点项目却总是腐化,程序员为什么会写烂代码?

    展开全文
  • 代码腐坏的味道是指在代码之中潜在问题的警示信号。并非所有的坏味道所指示的确实是问题,但是对于大多数坏味道,均很有必要加以查看,并作出相应的修改。 1. 重复的代码 如果你在一个以上的地点看到相同的...

    转自  http://www.nowamagic.net/librarys/veda/detail/1761

    代码腐坏的味道是指在代码之中潜在问题的警示信号。并非所有的坏味道所指示的确实是问题,但是对于大多数坏味道,均很有必要加以查看,并作出相应的修改。

    1. 重复的代码

    如果你在一个以上的地点看到相同的程序结构,那么当可肯定:设法将它们合而为一,程序会变得更好。

    • 同一个class内的两个函数中含有重复的代码段
    • 两个兄弟class的成员函数中含有重复的代码段
    • 两个毫不相关的class内出现重复的代码段

    注意:重复的代码是多数潜在BUG的温床!

    2. 过长的函数

    拥有短函数的对象会活的比较好、比较长。

    • 程序愈长就愈难理解
    • 函数过长阅读起来也不方便
    • 小函数的价值:解释能力、共享能力、选择能力

    原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立的函数中。记着,起个好名字!

    3.  过大类

    如果想利用单一类做太多事情,其内往往就会出现太多的成员变量。

    • 提取完成同一任务的相关变量到一个新的类
    • 干太多事情的类,可以考虑把责任委托给其他类

    注意:一个类如果拥有太多的代码,也是代码重复、混乱、死亡的绝佳滋生地点。

    4.  过长的参数列表

    太长的参数列表难以理解,太多参数会造成前后不一致、不易使用,而且你需要更多数据时,就不得不修改它。

    原则:参数不超过3个!

    5. 发散式变化

    我们希望软件能够更容易被修改。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这点,你就嗅出“坏味道:发散式变化”或“坏味道:霰弹式修改”。

    发散式变化:一个类受多种变化的影响

    • 数据库新加一个字段,同时修改三个函数:Load、Insert、Update
    • 新加一个角色二进制,同时修改四处

    原则:针对某一外界变化的所有相应修改,都只应该发生在单一类中

    6. 霰弹式修改

    如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改以响应之。如果需要修改的代码散布四处,你不但难以找到它们,也很容易忘记某个重要的修改。

    霰弹式修改:一种变化引起多个类相应的修改

    7. 依恋情节

    函数对某个类的兴趣高过对自己所处类的兴趣,就会产生对这个类的依恋情节,造成紧耦合。

    原则:判断哪个类拥有最多被此函数使用的数据,然后将这个函数和那些数据摆在一起。
    原则:将总是变化的东西放在一块。

    8. 数据泥团

    有些数据项,喜欢成群结队地待在一块。那就把它们绑起来放在一个新的类里面。这样就可以:

    • 缩短参数列表
    • 简化函数调用

    9. 基本型别偏执

    代码中有很多基本数据类型的数据。

    原则:如果看到一些基本类型数据,尝试定义一种新的数据类型,符合它当前所代表的对象类型。

    10. switch惊悚现身

    面向对象程序的一个最明显特征就是:少用switch语句。从本质上说,switch语句的问题在于重复。

    原则:看到switch你就应该考虑使用多态来替换它。

    11. 冗赘类

    你所创建的每一个类,都得有人去理解它、维护它,但一个类没有存在的必要时候,就让这个类庄严扑义吧!

    原则:如果一个类的所得不值其身价,它就应该消失。

    12. 夸夸其谈其未来性

    对未来不可预知的变化考虑的过多,造成系统更难理解和维护。如果应对变化的代码都会被用到,那是值得的那么做;如果用不到,就不值得。

    原则:代码应该满足当前的需求,并留有可扩展的余地。对于未来的变化,既不要考虑的太多,也不能一点都不考虑。

    13. 令人迷惑的暂时成员变量

    有时你会看到这样的对象:其内某个成员变量仅为某种特定的情形而设。这样的代码容易让人不解,因为你通常认为对象在所有时候都需要它的所有变量。

    在变量未被使用的情况下猜测当初设置目的,会让你发疯。

    14. 无用的中间人

    过度使用委托。你也许会看到某个类的接口有一半的函数都委托给其他类,这样就过度运用了。所以,删除无用的中间人。

    15. 狎昵关系

    有时你会看到两个类过于亲密,花费太多时间去探究彼此的private成分。

    原则:过分狎昵的类必须拆散。

    16. 异曲同工的类

    如果两个函数做同一件事情,却有着不同的签名式。

    原则:删除一个,保留一个。

    17. 不完美的程序库类

    库的设计有时会不够好,不那么容易使用,可能还会有那么一点小缺陷。

    工具:

    • Introduce Foreign Method
    • Introduce Local Extension

    18. 过多的注释

    过多注释的代码段,往往都是因为那段代码比较糟糕,散发着一股恶臭。

    原则:当你感觉需要写注释时,请尝试重构,试着让所有注释都变得多余。

    展开全文
  • 人人都要懂的代码重构

    千次阅读 2020-10-09 15:31:17
    因为代码腐化2.2 为什么代码会腐化2.2.1 破窗效应(Broken windows theory)和惯性2.2.2 技术债务(Technical Debt)2.3 防止代码腐化,重构应该怎么做2.3.1 重构的技术挑战2.3.2 重构的步骤2.3.3 重构的最佳时机2.4...

    1 心得体会

    1.1   重构是一件大事,也是一件小事。为了应对软件腐化,重构是一件重要的大事。但更是一件小事,要做在平时,“Baby Step”。重构不是积累而成的负担,而是软件工程师的日常行为习惯。

    1.2   我们以往对代码坏味道的识别太欠缺,一是从产品层面缺乏对代码坏味道的识别和跟踪,二是工程师识别坏味道的能力不足。

      对一个软件产品来说,测试通过、DI清零、没有网上问题就够了吗?研发团队即使达到了上面的要求,依然痛苦不堪,因为每次新需求的开发都要如履薄冰,每次“过点”都要重新煎熬一遍。随着规模增长,代码越来越腐化,修改成本越来越高,开发效率其实越来越低。曾经有很多质量指标,但缺少的恰恰是对代码本身质量的度量、对代码坏味道的识别。当前,不论是人工代码检视,还是工具自动检查,都应该利用工具将代码坏味道管理起来(说的是代码坏味道,而不是指标)。圈复杂度、行数等指标依然不够,这些指标只说明代码可能存在坏味道,但没有把坏味道列出来,跟踪改进。培训中提到了好的经验,就是识别坏味道(人工或者工具),并跟踪坏味道代码改进,就像跟踪问题单一样跟踪一段坏味道代码。

      另一方面,很多工程师没有意识到自己识别坏味道的能力不足,常常是看着代码但发现不了问题。这种现象是很突出的,在培训中,面对看上去很简单的案例代码,很多学员发现不了问题,认为那样写理所应当。而实际上,那段代码已经违反了一些基本的原则或规范。

    public String printReport(){
         StringBuffer reportinfo=new  StringBuffer();
         reportinfo.append("Report card for " + studentName +"\n");
         reportinfo.append("-------------------------------"+"\n");
         reportinfo.append("Course Title              Grade"+"\n");
         Iterator grades = clines.iterator();
         CourseGrade grade;
         double avg = 0.0;
         while(grades.hasNext()) {
            grade = (CourseGrade)grades.next();
             reportinfo.append(grade.title + "            " + grade.grade+"\n");
             if(!(grade.grade=='F')) {
                 avg = avg + grade.grade-64;
             }
         }
         avg = avg / clines.size();
         reportinfo.append("-------------------------------"+"\n");
         reportinfo.append("Grade Point Average = " + avg+"\n");
         return reportinfo.toString();
     }
    
    

    1.3   对重构工具的使用太少,应该利用工具提高重构效率。重构课程上,现场调查,知道eclipse的重构菜单的人很少(不到30%),大多数人在重构时没有使用过这类自动化重构工具,都是自己手动修改代码。重构代码过程中,往往涉及大量的代码替换、改名、搬移等操作,如果手工操作,很容易遗漏、出错,而如果使用工具自动完成,效率提升极大。这次培训中使用了eclipse中的提取方法(Extract Method)、提取本地变量(Extract Local Variable)、提取常量(Extract Constant)、内联(Inline)几个重构菜单,一键智能化完成所有相关修改点的修改,效率提升十倍。

    在这里插入图片描述

    eclipse的重构菜单

    1.4   针对遗留系统,首先做到“不伤害”老系统,即新旧隔离。方法有很,对于我们大部分产品都是基于已有系统增量开发,这一部分价值很大。新旧隔离的思想,工程师应该熟悉和深入理解、在工作中应用,让新代码不破坏旧系统,并保证新代码满足CleanCode。

    1.5   重构工作能够真正落实到工程师日常工作中,需要得到管理者的认可和支持,关键是内部质量数据可视。工程师进行代码重构,会占用交付的时间。重构工作的产出,在以往的价值评价中是看不到的。按时交付特性,解决了几个问题单,这些都是度量可见的,以往管理者更(甚至只)关注这些。要解决日常重构价值不可见的问题,关键是将内部代码质量数据可视,通过看板等呈现出代码质量的变化,让工程师看到,让管理者看到。我们曾经经常听到正面的案例是,某某没有软件背景的(或者刚来不久的新人)在很短时间完成了某紧急需求开发,几天交付云云,这就是英雄。但代码的内在质量如何呢?实际上可能很烂。堆代码看上去很“快”交付,但导致的腐化其实是巨额债务,欠债总是要还的。以往,这些债务对于管理者是难以直接看到和感知到的。殊不知,很多在研的、维护的、网上的疑难问题动辄拉上几个团队联合攻关;很多需求合入版本后质量不能快速稳定;每当过点问题DI起起伏伏…… 都是前期技术债务的体现。后来的人,多少通宵达旦其实就是在还债。目前,代码内部质量的重要性在公司内已经达成共识,让内部质量显性化,让改进可视、可感知尤为重要。

    1.6   CleanCode、单元测试、重构,应该是软件工程师的必备技能。一般新人进来后就重点培训这三项能力,具备这些能力后才能开展编码等相关工作。
      除了在公司内的赋能、实践和牵引外,工程师自身也应该加强自我提升。《重构——改善既有代码的设计》(下称《重构》)是软件重构领域最经典的书,从1999年英文版出版(2003年中文版出版)到现在,二十年来一直是重构类必读的书,没有替代者。在内部调查中,读过该书的并不多,不到30%。另外,在核心开发骨干的调查中,Committer、骨干,了解GoogleTest工具的人,占比也是很少,1/4左右,工程师对业界经典的单元测试/LLT工具及方法了解很少,实际工作中更难以有效运用单元测试/LLT。在繁忙的交付过程中,工程师也应尽量自己学习和提升必备技能。

    1.7   好代码是“打磨”出来的,不可能一蹴而就,“打磨”是即时进行的。“打磨”,也即重构,是在日常工作中完成,是工程师写完一个函数、一段代码后立即进行的,而不是等待其他“时机”。“我这次先合上去,下来一定再优化一下!”,经验告诉我们,让这种信誓旦旦的保证变为现实的可能性几乎为零。

    2 理解重构

    2.1 为什么要做重构?因为代码腐化

    代码腐化的外在影响主要有:

    1. 开发效率降低。代码可读性差,难以理解;代码牵一发动全身,不敢修改;越来越想大泥球,无法针对修改进行精准测试…… 都会导致开发效率降低。
    2. 质量下降,故障率上升,难以定位问题。不清晰的代码结构,使修改更容易引入问题,并且难以快速定位清楚问题原因,导致系统故障率逐步上升,新开发功能质量难以快速稳定。
    3. 无法有效支撑新需求。系统越来越耦合,新功能越来越难以“插入”这种耦合系统,不但功能难以高效扩展,性能也可能无法达成要求。

    代码腐化的具体表现有:

    1. 结构糟糕。例如函数圈复杂度过高、函数行数过大等。
    2. 安全隐患。例如内存泄漏/越界/非法访问、异常处理不合理、内部数据/接口暴露等。
    3. 扩展性差。随着功能的增加,如果没有很好控制耦合,会使后来的功能扩展越来越困难,典型的比如霰弹修改。
    4. 难以协同。如果系统存在越来越多的耦合,就难以进行多人、多团队独立开发,不同的开发者可能越来越多的面临冲突,并增大修改引入bug的可能性。
    5. 模块调试难。随意的修改和调用导致模块间的关联、依赖越来越多,模块间接口也开始模糊不清,会大大降低调试效率。
    6. 难以复用。相同的代码子不同模块或函数内重复出现,或者重复开发功能类似的子模块,都会使代码重复或者冗余,同时又可能跟所处上下文存在特殊关系,导致重复的内容也难以复用。而重复的内容又会带来修改遗漏引入bug等风险。
    7. 算法不当。主要指使用了不恰当的数据结构和算法,导致功能或者性能受、扩展性等受到影响。
    8. 性能问题。代码的不合理,也可能导致性能得不到满足,包括对突发规模(容量、访问量等)的应对问题。

    代码重构,就是要解决和预防代码腐化问题。

    2.2 为什么代码会腐化

    导致腐化的因素权重(%)
    需求变更多30%
    交付压力大时间紧30%
    架构不好15%
    保持与旧代码一致,但旧代码已经很烂10%
    管理问题10%
    工程师能力差5%

    通过调查,导致代码腐化的主要原有有哪些?以下将输出综合到一起的大概结论:

      其中,“需求变更多”、“交付压力大”几乎是排在TOP3的因素。也就是说,现在我们的工程师普遍认为,代码腐化主要的原因是活太多了,没时间把代码写好。这个理由乍听上去很合理,但首席的一句话,还是让人很有触动,他说:“工程师们普遍在找别人的原因、找外部的原因,好像都是别人的错。”

      不可否认,外部需求的变更、版本交付的压力,的确是导致代码腐化的重要原因,然而,从工程师角度来说,如果只看到这些原因,那就有点“甩锅”了,特别是,大部分人都没有把编码能力提出来作为代码腐化的重要原因,值得深思。是不是我们长期只关注交付的氛围导致工程师自己都不知不觉中忽视了编码能力提升的重要性?

      现实情况是,读过《重构》那本经典书的人占比不到30%(这还是乐观估计),了解业界流行的单元测试工具(例如GoogleTest)的人也很少,听过SOLID设计原则的也不多,很多人面对一段简单代码中的坏味道识别不出来(认为代码那样写理所当然)…… 上面这些例子,不是臆想,而是笔者近几年组织和参加多次软件训练营、重构培训时在现场调查中发现的普遍情况。值得注意的是,能参加那些专题赋能培训的人基本都是committer、MDE、开发骨干等。上述情况说明,我们的工程师在编码能力上还有不少提升空间。代码是工程师一行一行敲出来的,代码的腐化,特别是具体到函数、代码段中的坏味道,是直接出自工程师之手。代码腐化的原因有很多,除了外在压力外,工程师的编码能力也很关键。

    2.2.1 破窗效应(Broken windows theory)和惯性

    破窗效应:没有修复的破窗,导致更多的窗户被打破。

    惯性定律:好代码会促生好的代码;糟糕的代码也会促生糟糕的代码。

    破窗效应和惯性定律从社会心理学角度解释为什么代码会越来越腐化。

    对工程师的伤害:如果工程师每天都与糟糕的代码相伴,会逐渐失去对代码坏味道的识别能力,并养成不好的编码习惯,久而久之,工程师的基础能力也会下降。

    适应现象:生理学上的适应现象发生在感官的末端神经、感受中心的神经和大脑的中枢神经上,适应的结果是感官对刺激感受的灵敏度急剧下降。嗅觉器官若长时间嗅闻某种气体,就会使嗅感受体对这种气体产生适应,敏感性逐步下降,随着刺激时间的延长甚至达到忽略这种气味存在的程度。

    反过来看,应该怎么做才能防止代码腐化?

    • 发现破窗,立即修复。发现坏味道,立即重构,最好在刚写完一个函数就立即审视有没有坏味道,是否有必要优化。
    • 学习好代码、写好代码,让好代码促生好代码,正向反馈。

    2.2.2 技术债务(Technical Debt)

    技术债务:开发人员为了加速软件开发,在应该采用最佳方案时进行了妥协,改用了短期内能加速软件开发的方案,从而在未来给自己带来的额外开发负担。这种技术上的选择,就像一笔债务一样,虽然眼前看起来可以得到好处,但必须在未来偿还。(1992年,Ward Cunningham首次将技术的复杂比作为负债

      欠债总是要还的,不是自己主动还(还好过一些),就是被逼债(更痛苦一些)。

      实际上Ward Cunningham的原话更激进一些,他认为第一次提交代码就已经开始欠债了,要通过不断重写(重构)来偿还债务,小额债务可以加速开发。但久未偿还债务会引发危险。

      Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite. Objects make the cost of this transaction tolerable. The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation, object-oriented or otherwise.

    2.3 防止代码腐化,重构应该怎么做

    重构:使用一系列的重构手法,对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。(Martin Fowler)

    2.3.1 重构的技术挑战

    1. 如何发现重构点(识别坏味道) *
    2. 知道重构的目标
    3. 如何去重构——重构技法 *
    4. 如何保证重构的正确性——单元测试 *

      当前,识别坏味道、熟练掌握重构技法、有效的开发自测(LLT),是从工程师能力角度出发,保证重构成功的关键,也是目前的短板。

    2.3.2 重构的步骤

    1. 通过自动化验证所有测试例通过(Verify that all automated tests(microtests) pass)
    2. 确定要重构的代码(Decide what code to change)
    3. 仔细地完成一处或多处修改(Implement one or more refactoring carefully)
    4. 随时运行微测试,确认修改没有改变系统的行为(Run the microtests whenever you wish to confirm that changes have not altered system behavior)
    5. 重复上面的步骤,直到到完成重构或回退到之前的状态(Repeat until the refactoring is complete or revert to an earlier state)

    上面的步骤概括解读一下:

    • 首先要有测试例保证。
    • 修改一点就自动化测试一点,每次修改都确保系统功能可用。
      注意,这里说的是“每次修改”都不影响系统功能,而不是整个重构结束后确保系统功能不变!也就是说,在重构的过程中如果被打断了,代码也是可用的。因为每次修改都经过快速的自动化验证,验证通过了进行一次“commit”,这样就保证了仓库中的代码是持续可用的,使整个重构可以“随时中断”、“断点续传”。
    • “Baby Steps”小步快反馈,不断重复“修改”+“验证”。典型的重构修改和验证应该是秒级或分钟级的。

    来看看大师Robert C. Martin是怎么说的:

      “我写函数时,一开始都冗长而复杂,有太多缩进和嵌套循环,有过长的参数列表,名称是随意取的,也会有重复的代码。
      “但是,我会配上一套单元测试,覆盖每行丑陋的代码。
      “然后我打磨这些代码,分解函数,修改名称,消除重复,缩短和重新安置方法,有时我还拆散类。同时保持测试通过。
       “最后,遵从这些规则,我组装好这些函数。”

    2.3.3 重构的最佳时机

      重构是持续进行的,不要先编写烂代码再重构。重构的最佳时机,不是在项目结束时、版本某个时间点、专门安排某个重构迭代,甚至不是每天下班时才重构。重构应该是伴随工程师编码过程中的,每隔一个小时或者半个小时,甚至每写一段代码或者一个函数后就要去做的事情。重构即时进行,代码持续干净。

    2.4 关于重构的“编程价值观”

      关于代码是否需要重构,我们应该纠正一种观点,那就是多一事不如少一事。修改可能会引入问题,所以一段代码功能实现了,即使写的很难看,但没有出过问题,也不应该修改,应该保持现状,万一改坏了,不是没事找事吗?其实,正是这种思想,使代码持续腐化。

      Martin Fowler 的观点是,如果一段凌乱的代码不但不需要修改,甚至都不需要理解它,那可以不重构(如果重写比重构还容易,那当然选择重写,这里讨论的是重构)。言外之意,一段代码即使不需要修改,但只是需要你去理解它做了什么,如果它很凌乱,你难以理解,那也是应该进行重构的。再简单说,只要你看到了坏味道,就应该重构它,除非你根本不需要看这块代码(这里好像矛盾,你不需要看它,为什么你还要点开看它呢,好奇心? :-> ),例如被封装在API里的永远不会被翻出来看和理解的稳定代码可以暂不重构。

      软件工程师应该具备一点“代码洁癖”,像一个匠人,随时“打磨”自己的作品。软件功能追求准确无误,代码开发过程就像艺术品一样反复打磨,整洁、易读、易扩展、易维护……。

    “整洁的代码简单直接。整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。”(Grady Booch,UML创始人之一、《面向对象分析与设计》作者)

    2.4.1 构建好代码文化从TOP10准则普及开始

      编码原则如何普及成为工程师的共识?编码的原则有很多,建议将最重要的TOP10原则作为普及的重点。让工程师每天都能看到,从“脸熟”开始,笔者分享了以下做法可供参考。

    • 某米公司,将工程师TOP10编码原则做成电脑屏保
    • 某公司,将TOP10原则做成卡片放到每个人工位
    • 某公司,将TOP编程原则做成摆件,送给工程师
    • ……

    3 代码坏味道

      代码坏味道不一定是bug,是代码不合理的地方,是让人难以理解或易于误解的代码,或者是不合理的代码结构,或者是暗含风险的代码逻辑……

    经典的代码坏味道举例:

    • 重复代码
    • 过长的方法
    • 过长的参数列表
    • 注释过多
    • 临时变量过多
    • Switch语句
    • 霰弹式修改
    • 基本类型偏执
    • 复杂表达式
    • 数据泥团
    • 冗余类
    • ……

    详见:22种常见代码坏味道

    业界大部分坏味道的总结,都是从《重构》书中来的。

    识别代码坏味道,是重构的最重要一步。

    3.1 识别坏味道

      前面已经说过,识别代码的坏味道,是目前我们工程师的一个短板。要么代码坏味道近在眼前却熟视无睹;要么可以意识到眼前的代码很烂,却说不出烂在哪里、如何重构,可以说相当“词穷”。所以,我们要培养对坏味道的敏感程度。
      在对资深工程师的统计调查中,一口气能够写出10种代码坏味道的人并不多。建议是,每个软件工程师都应该掌握30+种代码坏味道,看到一段代码,能马上嗅出存在哪种坏味道,知道应该怎么重构改进。

    3.2 跟踪代码坏味道

      人工检视或者工具扫描识别的代码坏味道,应该得到跟踪改进。这里说的是坏味道代码本身,不只是行数、圈复杂度等统计度量数据,而是那段“烂代码”。笔者介绍了他所在公司对代码坏味道跟踪的内容,可供参考,除了这段代码的位置外,还包括:

    • 代码坏味道(Code Smell)——坏味道名称
    • 症状(Symptoms)——有助于找出问题的线索
    • 原因(Causes)——对问题如何发生的说明
    • 采取的措施(WhatToDo)——可能的重构方法
    • 收益(PayOff)——在哪些方面有所改善
    • 不适应情况——在哪些情况下不适用这种重构

      就像问题单电子流一样,跟踪坏味道代码,并且给出重构的建议。识别了坏味道代码,并有对该坏味道的重构方法建议,可以使工程师很方便高效地进行重构。

    4 重构名录

    4.1 重构方法列表

    经典的重构方法目录,即《重构》一书作者总结的几十种方法,详见:https://www.refactoring.com/catalog/
    本地摘要: 重构方法摘要

    4.2 重构与模式

      重构的方法有很多,大致分为两类:一般的重构方法(微重构)和设计重构(基于模式的重构)。针对一种代码坏味道,往往可以使用一般的微重构方法,如果微重构不满足要求,也可以使用模式重构。

    例如:

    代码坏味道一般重构方法使用模式重构
    重复代码提炼方法、提取类、方法上移、替换算法、链构造方法构造Template Method、以Composite取代一/多之分、引入Null Object、用Adapter统一接口、用Factory Method引入多态创建

    更多的重构与模式,详见这里

    5 常见重构案例

    5.1 函数

    只要打开代码,几乎无时无刻不跟函数打交道。如何把代码写简洁、做好封装和抽象、避免重复等,围绕函数有很多方法可以使用。

    5.1.1 单一抽象层次原则(Single Level of Abstraction Principle,SLAP)

    “让一个方法中的所有操作处于相同的抽象层。” (Robert C. Martin,《Clean Code》)

    遵循了单一抽象层次原则,函数的主流程将更加清晰。

    举例:下面代码段1和代码段2中的 add 函数,哪个更易读呢?注意代码段1中6~11行与代码段2的比较。

    • 代码段1:
    public void add(Object element) {
        if (readOnly){
            return;
        }
    
        if (size + 1 > elements.length) {
            Object[] newElements = new Object[elements.length + 10];
            for (int i = 0; i < size; i++)
                newElements[i] = elements[i];
            elements = newElements;
        }
    
        addElement(element);
    }
    private void addElement(Object element) {
        elements[size++] = element;
    }
    
    • 代码段2:
    public void add(Object element) {
        if (readOnly){
            return;
        }
    
        if (shouldGrow()) {
            grow();
        }
    
        addElement(element);
    }
    private boolean shouldGrow() {
        return size + 1 > elements.length;
    }
    private void grow() {
        Object[] newElements = new Object[elements.length + 10];
        for (int i = 0; i < size; i++)
            newElements[i] = elements[i];
        elements = newElements;
    }
    private void addElement(Object element) {
        elements[size++] = element;
    }
    

    5.1.2 意图与实现分离

    “如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。”(Martin Fowler,《重构》)

    我们经常听到两个人的对话中,一位说我要干这个、做那个…… 说着说着被对方打断:“你到底想干什么?”看代码也是,当我们看到一长段代码细节,不由得皱起眉头,这代码到底要干啥?意图与实现分离,将实现细节隐藏在子函数中,可以让代码可读性大大增强,就像看一本书,只要看看目录就知道书大概写了什么内容,如果没有目录、子目录,整整一本正文,要想快速知道书的内容就太难了。

    把“意图与实现分离”与“单一抽象层次原则结合起来”,不断向下层迭代,你会发现,代码被分层,并且每个函数都变得很短小。而短小的函数,也是我们所推崇的。

    “在我看来,一个函数一旦超过6行,就开始散发臭味。我甚至经常会写一些只有1行代码的函数。”(Martin Fowler,《重构》)

    在上一小节“单一抽象层次原则”的示例中,同样体现了“意图与实现分离”的原则。

    5.1.3 常用方法:提取子程序/函数

    提取子程序/函数可以带来很多好处:

    1. 降低复杂度
      度量函数复杂度的方法有多种,例如:
      o 代码行数
      o 参数个数
      o 调用其它函数/对象/包的数量
      o 每行运算符的数量
      o 跳转语句的个数(go/break/continue/throw ……)
      o 控制结构中的嵌套层数
      o 变量个数(局部变量、全局变量)
      o 同一变量的先后引用之间的代码行数(跨度)
      o 变量生存的代码行数
      o 递归
      o 预编译的数量(c语言)
      o 函数出口(return)数量
      o 注释比例
      o 分支语句比例
      o ……

    我们目前最常使用的度量指标是圈复杂度,即一个方法/函数中执行路径的数量。函数的圈复杂度最好不超过10

    正常程序员看上去很难处理好5~9个以上的智力实体,并且提高的可能性不大,因此你只有减低你程序的复杂度。(Miller,1995,计算机科学家,ACM Paris Kanellakis 奖获得者)

    创建子程序/函数可以有效隐藏一些信息,使原有代码复杂度降低。

    1. 封装变化
      将易变的内容封装到函数中,可以提升可维护性。当修改子函数的内容或者用另一个子函数做替换时,不会影响其他代码。
    2. 引入中间的,易懂的抽象
    3. 避免代码重复
      当多处代码类似时,用一些技巧提取公共函数,之后再做修改时,可以只针对公共函数内部修改,从而避免修改遗漏,降低修改难度。
    4. 简化复杂的布尔判断
    5. 支持子类化(subclassing)
    6. 装换/适配
    7. 提高可移植性
    8. 改善性能
    9. 占位(TODO)
    10. 方便测试
    11. 隐藏全局数据
    12. 隐藏顺序
      把带有执行顺序的一块代码放到函数中,比让它们在系统中到处散布要好得多,减少修改出错。
    13. 隐藏指针操作
    14. 递归
    15. ……

    5.1.4 函数10个一

    笔者分享的其所在公司倡导的函数“10个一”:

    1. 每个变量只用于单一用途
    2. 每一行代码只表达一件事
    3. 一个循环只做一件事
    4. 单一抽象层次原则
    5. 代码组织得一次只做一件事
    6. 一种变化仅仅修改一处
    7. 函数应该遵守单一职责
    8. 函数圈复杂度应该小于一十
      概括为一句话就是:说话简单点、直接点!

    构造软件设计有两种方法:一种是简单,明显地没有缺陷;另一种方法是使其复杂,却没有明显的缺陷。(Tony Hoare,图灵奖得主,快速排序算法、霍尔逻辑等的设计者)

    1. 函数第一原则是必须要短小
    2. 编写函数时必须一心一意,专注,怀有谦卑的心态

    案例1:
    下面函数中存在哪些坏味道?

    void printValues() {  
        double averageAge = 0; // 平均年龄  
        int totalGrade = 0; // 总成绩  
    
        for (int i = 0; i < students.length; i++) {  
            averageAge += students[i].getAge();  
            totalGrade += students[i].getGrade();  
        }  
        averageAge = averageAge / students.length;  
    
        System.out.println("averageAge = "+averageAge);  
        System.out.println("totalGrade = "+totalGrade);  
    }
    

    坏味道:

    • 违反单一职责原则,一个函数中既计算年龄、又计算成绩,最终目的是打印
    • 一个循环中做了两件事
    • averageAge变量具有两个不同含义,总和、平均值
    • 在意图与实现分离方面可以进一步优化,使主流程更清晰

    重构后代码:

    void printValues() {
        System.out.println("averageAge = "+averageAge());  
        System.out.println("totalGrade = "+totalGrade());  
    }
    private double averageAge() {  
        double result = 0; // 平均年龄  
        for (int i = 0; i < students.length; i++) {  
            result += students[i].getAge();  
        }  
        return result / students.length;  
    }
    private int totalGrade() {  
        int result = 0; // 总成绩  
        for (int i = 0; i < students.length; i++) {  
            result += students[i].getGrade();  
        }  
        return result;  
    }
    

    案例2:

    class Counter {
        private static int count = 0;
    
        public int getCount() {
            return ++count;
        }
    }
    
    

    上面代码存在哪些隐患呢?

    • 存在bug的可能,每次查询都会使计数改变
    • 查询与操作没有分离,违反单一职责原则,函数做了不只一件事

    5.1.5 展开讨论:每一行代码只表达一件事

    Linux内核编码规范:
    不要将多个语句放在同一行,除非你要掩饰什么!
    Don’t put multiple statements on a single line unless you have something to hide!

    也许有人会认为一行代码包含多个语句有如下好处:

    • 一屏可以多看几行代码
    • 缩短了函数的行数,看上去更简洁
    • 为编译器提供了优化线索,提高性能

    实际上,上述三点都存在问题。首先,如果一行包含多条语句,虽然看上去一屏可以多看几行代码,但实际上看代码的效率是降低了,因为你要花更多精力去分析一行中的多条语句。其次,通过一行代码多条语句缩短函数行数的做法有点自欺欺人,因为复杂度没有变化,反而隐藏了复杂性的直观性。最后,通过一行多条语句的方式,不会给编译器带来任何优化线索,格式编排优化不了编译性能。

    相对于追求最小化代码行数,一个更好的度量方法是最小化人们理解它所需要的时间。

    一行代码包含多个语句带来的问题:

    • 增加阅读难度
    • 编译错误难以定位
      编译报错时,这一行有多条语句,到底是哪条出错了呢?
    • 调试时按行运行无法执行单个语句
    • 扩大了代码修改的影响范围,易出错
      如果一行代码包含多个语句,但你只希望修改其中一个语句,修改时更容易误操作改动同一行的其他语句。另外,你想为其中一个语句增加注释也很困难。
    • 隐藏了程序的复杂性

    感受一下一行代码多条语句的威力(让你的头变大 :> ):

    while(i<n) {int id = getId(i); printStudent(getStudentInfo(name[i++], id));}
    return a[++i] + (i>(n++))?*p:0;
    

    一行代码只表达一件事带来的好处:

    • 便于阅读理解
      阅读代码只需要从上而下,不需要从左往右寻找语句,一目了然。甚至你只需要阅读前几个关键字就知道这行代码的含义。
    • 更直观的代码复杂性展示
    • 更方便地按行号找到编译错误
    • 更方便的按行单步调试
    • 便于修改,需要改哪行就改哪行,避免对其他代码语句的影响

    5.1.6 展开讨论:函数第一原则是必须要短小

    一个函数的最大长度和该函数的复杂度、缩进级数成反比。

    Martin Fowler认为,一个函数超过6行就开始散发臭味,他甚至经常会写只有1行代码的函数。

    1. 函数调用过多会影响性能吗?
      有人会担心短函数造成大量函数调用,影响性能,其实这种担心大部分情况是没有必要的。由于函数调用而影响性能的情况已经非常罕见了。

    “短函数常常能让编译器的优化功能运转更良好,因为短函数可以更容易地被缓存。”(Martin Fowler,《重构》)

    另外,现代编译器的智能优化,会把一些短函数在编译时进行内联(inline),把短函数再展开,避免过多层调用。

    当然,如果程序对性能的要求达到了苛刻的程度,达到了要考虑函数调用带来的开销,那当然要考虑函数调用次数这个指标。

    1. 只有一行代码的函数有意义吗?
      如果一行代码已经完成了一件事,那么应该封装为函数。

    如下两段代码对计数的增加,哪一段更合适呢?
    代码段1:

    packageCounter[packageType]->errorCounter[ERROR_TIME_OUT]++;
    

    代码段2:

    addTimeOutCount(packageType);
    

    代码段2封装为函数的好处:

    • 更易读,函数名就已经表达了意图
    • 更易于修改、替换
      -1 如果代码中有多处count处理,那么只需要修改函数内部一处即可。
      -2 如果count的处理完全变了,例如数据结构修改、字段修改,甚至计数方法修改,那么只需要替换函数内容即可。

    5.1.7 SOFA原则

    • Short:保持简短,以便能迅速获取主要目的
    • One:只做一件事情,以便测试能集中于彻底检查这件事
    • Few:少量的输入、参数、变量、逻辑……,以便非常重要的值组合都能被测试到
    • Abstraction:抽象层次一致,以便它不会在做什么和怎么做之间来回跳转

    5.2 复杂表达式

    复杂表达式的典型情况:

    • 复杂的条件逻辑
      条件表达式中堆积计算过程
    • 复杂的布尔表达式
      大量的andornot

    5.2.1 引入解释性变量(Introduce Explaining)

    将复杂表达式的结果放入临时变量,以临时变量的名字来解释表达式的含义

    if ((platform.toUpperCase().indexOf("MAC") > -1)
        && (browser.toUpperCase().indexOf("IE") > -1)
        && resize > 0) {
        // do something
    }
    

    重构后:

    final boolean isMacOs     = platform.toUpperCase().indexOf("MAC") > -1;
    final boolean isIEBrowser = browser.toUpperCase().indexOf("IE")  > -1;
    final boolean wasResized  = resize > 0;
    
    if (isMacOs && isIEBrowser &&  wasResized) {
       // do something
    }
    

    5.2.2 将复杂的表达式做成布尔函数

    Day today = time.dayOfWeek;
    Day d = event.day;
    if ((d == EVERYTDAY) || (d == today)
        || (d == WEEKEND && (today == SATURDAY || today == SUNDAY))
        || (d == WEEKDAY && (today >= MONDAY && today <= FRIDAY))) {
        // is wakeup day, then
        // turn on light
        lightController(event.id, TURN_ON);
    }
    

    重构后:

    Day today = time.dayOfWeek;
    Day d = event.day;
    
    if (isWakeUpDay(d, today)) {
        lightController(event.id, TURN_ON);
    }
    bool isWakeUpDay(Day d, Day today) {
        if (d == EVERYTDAY)
            return TRUE;
        if (d == today)
            return TRUE;
        if ((d == WEEKEND && (today == SATURDAY || today == SUNDAY))
            return TRUE;
        if ((d == WEEKDAY && (today >= MONDAY && today <= FRIDAY)))
            return TRUE;
        return FALSE;
    }
    

    5.2.3 分解条件式(Decompose Condition)

    面对复杂的条件表达式,对条件判断和每个条件分支分别提取为函数。

    if (!data.isBefore(SUMMER_START) && !data.isAfter(SUMMER_END))
        // summer
        charge = quantity * _summerRate;
    else
        charge = quantity * _winterRate + _winterServiceCharge;
    

    重构后:

    if (summer())
        charge = summerCharge();
    else
        charge = winterCharge();
    
    bool summer() {
        return (!data.isBefore(SUMMER_START) && !data.isAfter(SUMMER_END));
    }
    
    Charge summerCharge() {
        return quantity * _summerRate;
    }
    
    Charge winterCharge() {
        return quantity * _winterRate + _winterServiceCharge;
    }
    

    5.2.4 以卫语句取代嵌套表达式(Replace Nested Conditional with Guard Clauses)

    条件表达式通常有两种风格,一种是:两个条件分支都属于正常行为,if 和 else 分支同等重要;另一种是:一个分支是正常行为,另一个分支是异常情况。对于第二种情况,推荐使用卫语句单独检查异常情况并立刻返回。

    或者这样理解:如果你关注的东西是多层嵌套后才执行的代码,那么为什么不先把不关注的情况排除掉呢?以减少嵌套层次。

    public void add(Object element) {
        if (!readOnly) {
            if (shouldGrow()) {
                grow();
            }
            addElement(element);
        }
    }
    

    重构后:
    readOnly 的判断使用卫语句处理。

    public void add(Object element) {
        if (readOnly){
            return;
        }
    
        if (shouldGrow()) {
            grow();
        }
        addElement(element);
    }
    

    5.2.5 表驱动法

    表驱动法是一种编程模式,从表中查找信息而不是使用逻辑语句(if/else、switch/case)。如果针对同一类型的不同值判断条件很多,使用表驱动法可以有助于降低复杂度,提高可扩展性;否则,表驱动法会增加复杂度。

    判断条件转换为表的索引,使用索引直接查表得到数据或者函数指针等。

    const int monthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}int getMonthDays(int month) {
        return monthDays[month-1];
    }
    

    如上代码,如果用if/else或者switch/case来判断月份返回天数的话,代码会很长,复杂度也会提升。

    从表中查询函数指针的例子:

    typedef void* (*FuncCreatePackageBody)(void* extendData, int* returnSize);
    
    typedef enum PackageType {
        PACKAGE_ICMP = 0,
        PACKAGE_TCP,
        PACKAGE_UDP,
        // ...
        PACKAGE_TYPE_MAX
    }PackageType;
    
    // 不同报文类型的创建函数表
    FuncGetPackage createPackageBody[] = {
        icmp_createPackage,
        tcp_createPackage,
        // ...
        unknown_createPackage,
    };
    

    代码中的查表用法:

        // ...
        FuncCreatePackageBody funCreatePackage = createPackageBody[packageType];
        void* package =  funCreatePackage(extendData, returnSize);
        // ...
    

    5.3 其他注意事项

    • 函数、变量名等尽量要用肯定语句形式
    • 尽量使用肯定形式的布尔表达式
      表达式中过多的否定会导致理解困难,感受一下这句话:“我并非不是一个不傻的人”
    • 尽量少用/慎用breakcontinue语句
      breakcontinue 在一些复杂逻辑中容易使程序员犯错。
    while (x) {
        switch(y) {
            case A:
                // ...
                if(z)
                    continue;
                break;
            case B:
                // ...
                break;
        }
    }
    

    6 基于复杂遗留系统的开发和重构

    在遗留系统上进行新增功能时,要防止软件退化。修改方案综合评估,包括不限于考虑:

    • 修改风险(与原有代码的修改范围、修改量等)
    • 可测试性
    • 可读性
    • 复用
    • 可扩展性
    • 开发效率

    6.1 常见的问题:缠绕(Tangling)

    代码不是一下就腐化的,最开始的结构往往是清晰的。但后来接到需求后直接往现有的的模块、对象、函数、方法里面塞代码,加了几个需求后,代码结构逐渐面目全非。

    主要的原因有:

    • 工程师不关注“隔离”
    • 工程师不知道用好的手段进行隔离
    • 软件本身就是个多维度的复杂体,即使有很聪明的开发者,可能也很难完全避免缠绕

    6.2 关键思路:分离关注点,新旧分离

    First, Do No Harm. 首先,做到不伤害。(Hippocrates,古希腊医学之父)

    常见解决方案:

    1. 新生方法
      在新的方法、函数中增加新功能
    2. 新生类
      可以是具体类、接口(策略模式)、接口集合(观察者模式)
    3. 外覆方法
      o 将旧接口封装在新接口中,并在新接口中增加功能
      o 老函数改名;新函数用老函数的名字,老代码的调用处不感知变化
    4. 外覆类
      o 新增类,调用已有类,并增加新功能
      o 新类继承老类,增加新功能
      o 委托(代理模式)
      o AOP(Aspect Oriented Programming,面向切面编程)
      o 装饰者模式

    针对历史遗留代码如何修改、新增功能,推荐书籍:《修改代码的艺术》

    7 重构管理——管理者的视角

    重构这项工作,往往在我们的工程师眼里属于出力不讨好的,重构好了,没人知道;修改出问题来了,拿你开刀。主要原因是,管理者视角难以看到重构工作的价值。因为理论上,重构不改变软件的外在表现,软件不会因为经过了重构就增加了功能、完成了新的需求,或者立即缩短了交付周期;相反,重构会增加工程师在现阶段的工作量。

    重构改进的是内部质量,其价值面向的是未来,是为了遏制和预防当前以及未来的代码腐化,是为了使开发效率不至于越来越低。重构属于前人栽树后人乘凉型的工作。

    重构带来的价值:

    • 提升代码内部质量,有利于减少bug,进而提升外部质量
    • 提升开发效率,使代码具有更好的可读性、易修改,提升软件的可扩展性,进而更快交付新需求

    从管理这的视角来看,当前需要补足的是对重构价值的可视化,也即代码内部质量的可视化,形成对重构价值的反馈。

    • 宏观上的代码内部质量
      圈复杂度、行数等指标
    • 微观上的代码内部质量
      各种代码坏味道

    建议:通过看板可视化、量化代码内部质量,使工程师和管理者都可见。代码内部质量的改进可度量。

    举例:

    • 某移动应用开发商,代码质量监控看板,呈现各种代码内部质量指标,“十大烂人榜”……

    业界代码内部质量看板开源工具:Sonargithub开源项目
    推荐书籍:《程序开发心理学》


    展开全文
  • 程序的腐化

    2018-05-25 09:30:26
    今天我们来说说,一个代码模块的代码是如何一步步腐化变质,到最后程序员都不愿意去维护它,然后要么重构,要么废弃换新模块的? 代码是有一定的周期的,这个没有错。为什么有的代码跑上几十年任然好用,而现在...
  • 今天我们来说说,一个代码模块的代码是如何一步步腐化变质,到最后程序员都不愿意去维护它,然后要么重构,要么废弃换新模块的? 代码是有一定的周期的,这个没有错。为什么有的代码跑上几十年任然好用,而现在...
  • 代码防腐

    2021-03-03 00:16:25
    代码腐化似乎注定的最初:没有谁是不想好好写的。都有一个宏伟的规划,这次一定途中:Code Review 如同“堂吉诃德”一般,根本架不住大批量大批量的修改放弃:躺平了,下次一定如此循环往复...
  • 谈程序的腐化

    2017-05-09 12:09:00
    今天我们来说说,一个代码模块的代码是如何一步步腐化变质,到最后程序员都不愿意去维护它,然后要么重构,要么废弃换新模块的? 代码是有一定的周期的,这个没有错。为什么有的代码跑上几十年任然好用,而现在...
  • 向工程腐化开炮:Java代码治理

    千次阅读 2022-03-11 14:58:50
    优酷腐化治理系列文章第三篇。
  • 2019独角兽企业重金招聘Python工程师标准>>> ...任何人类的设计都会腐化,软件...通用语言,领域模型和代码目标意图一致 转载于:https://my.oschina.net/u/1000241/blog/3056689
  • 正文经历了几个从商业角度来看或成功或失败的项目,都会发现代码、设计都会慢慢地、在不经意间腐化。而且有一个项目开始的时候,架构是经过精心设计的,也有较为严格的代码规范,并且通过静态代码检查...
  • A:呃,那是以前的代码,所以我没动,我只是在这个类里加了个新方法.... 重读Clean Code,忽然对Bob大叔提到的童子军军规深有感触:“让营地比你来时更干净”。 代码总是随着时间的流逝,需求的增加而...
  • 优秀的代码腐化代码区别在哪里?怎么让自己写的代码既漂亮又有生命力?接下来将对代码质量的问题进行一些粗略的介绍。也请有过代码质量相关经验的朋友提出宝贵的意见。代码质量所涉及的5个方面,编码标准、代码...
  • 欢迎来到GitHub Pages 您可以使用的来维护和预览Markdown文件中网站的内容。 每当您提交到该存储库时,GitHub Pages都将运行从Markdown文件中的内容重建站点中的页面。降价促销Markdown是一种轻巧且易于使用的语法,...
  • 软件工程的内部质量标准有:可维护性,灵活性,可移植性,可重用性,可测试性,...不符合以上标准都可以称之为代码腐化,形象的理解就是一个苹果,从内部开始烂了,烂到原本应该负责内部代码的程序员拒绝去维护了。...
  • 2011年刚进入一个新部门,接手一个老项目,典型的遗留代码 , 一个jsp 好几千行,那叫一个乱。但是细细瞧瞧, 还有不少代码是不错的,依稀能看到漂亮代码的影子,可以想象...
  • 目录 介绍 定律 阿姆达尔定律 (Amdahl's Law) 布鲁克斯法则 (Brooks's Law) 康威定律 (Conway's Law) 侯世达定律 (Hofstadter's Law) 技术成熟度曲线 (The Hype Cycle &am...
  • 易维护、易读、易扩展、灵活、简洁、可复用、可测试的代码就是高质量的代码,而高质量代码的达成路径工具箱包括:面向对象设计思想是基本指导思想,是很多设计原则、设计模式的实现基础;设计原则是代码设计的抽象...
  • 设计模式之代码重构

    2022-06-15 11:04:27
    设计模式之代码重构
  • 如果不积极地修改、挽救,随着时间流逝,所有软件都会不可避免地渐渐变得复杂、难以理解,最终腐化、变质。因此,理解并修改已经编写好的代码,是每一位程序员每天都要面对的工作,也是开发程序新特性的基础。然而,...
  • 如何写好代码注释?

    2022-02-04 10:49:47
    对于代码注释来说,在不同的教程或者原则中有不同的规定或者解释。有的原则是需要使用JavaDoc来描写每个方法,而有的原则是要求每一个属性标注命名。我愿意相信每一份看起来不那么妥当的注释都是出于一些善意的目的...
  • 《重构:改善既有代码的设计》C++版本,相互讨论,共同学习,不断成长

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,540
精华内容 1,016
关键字:

代码腐化