项目重构演进之路
目录介绍
01.整体概述说明
1.1 重构的背景
1.2 重构的要求
1.3 遇到问题
1.4 重构的目的
1.5 设计目标
1.6 产生收益分析
02.重构的具体实践
2.1 重构什么
2.2 何时重构
2.3 思考如何重构
2.4 针对复杂场景
03.重构技术手段
3.0 举一个重构例子
3.1 罗列重构事项
3.2 把握关键节点
3.3 编写测试用例
3.4 mock 业务数据
3.5 发现代码 bug
3.6 优化编码方式
04.避免重构失败
4.1 能否给充足理由
4.2 乱套设计模式
4.3 先有问题后改造
05.架构设计思考
5.1 针对复杂场景
5.2 如何做架构设计
01.整体概述说明
1.1 重构的背景
项目的代码往往牵一发而动全身,业务逻辑耦合严重。
对于大的架构重构,其实一直很谨慎的。原则是将重构融合在每次迭代中,逐步优化代码的结构。然后将这个工作持续进行下去!
当初设计的架构让项目的依赖关系越来越复杂,维护成本也越来越高。
决定梳理并优化一下整个项目结构。在实施过程中,依然坚持将整个重构的过程融合在每个迭代中,逐步完成一次大的架构升级。
1.2 重构的要求
重构代码对一个工程师能力的要求,要比单纯写代码高得多
重构需要你能洞察出代码存在的坏味道或者设计上的不足,并且能合理、熟练地利用设计思想、原则、模式、编程规范等理论知识解决这些问题。
提高代码的质量
具体点说就是,提高代码的可读性、可扩展性、可维护性等。
多问自己为什么这样设计
在做代码设计的时候,一定要先问下自己,为什么要这样设计,这样做是否能真正地提高代码质量,能提高代码质量的哪些方面。
如果自己很难讲清楚,或者给出的理由都比较牵强,没有压倒性的优势,那基本上就可以断定这是一种过度设计,是为了设计而设计。
1.3 遇到问题
项目痛点在哪里
先要去分析代码存在的痛点,比如可读性不好、可扩展性不好等等,然后再针对性地利用设计模式去改善。
对重构理解不深入
对为什么要重构、到底重构什么、什么时候重构、又该如何重构等相关问题理解不深,对重构没有系统性、全局性的认识。
对重构没有技巧
面对一堆烂代码,没有重构技巧的指导,只能想到哪改到哪,并不能全面地改善代码质量。
1.4 重构的目的
软件设计大师 Martin Fowler 是这样定义重构
重构是一种对软件内部结构的改善,目的是在不改变软件的可见行为的情况下,使其更易理解,修改成本更低。
重构的定义很重要
有一个值得强调的点:“重构不改变外部的可见行为”。把重构理解为,在保持功能不变的前提下,利用设计思想、原则、模式、编程规范等理论来优化代码,修改设计上的不足,提高代码质量。
遇到问题再重构
维护代码的过程中,真正遇到问题的时候,再对代码进行重构,能有效避免前期投入太多时间做过度的设计,做到有的放矢。
1.5 设计目标
重构围绕一个老生常谈的概念「解耦」「拓展」「维护」等维度展开,设定几个目标:
清晰划分各模块的角色
明确架构层级及各个模块所在的层级
提高整个架构横向扩展的能力
各模块独立开发,面向接口和协议编程
提高代码可维护性和可读性
02.重构的具体实践
2.1 重构什么
根据重构的规模
可以笼统地分为大规模高层次重构(以下简称为“大型重构”)和小规模低层次的重构(以下简称为“小型重构”)。
大型重构指的是对顶层代码设计的重构
包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。
这类重构的工具就是使用学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入 bug 的风险也会相对比较大。
小型重构指的是对代码细节的重构
主要是针对类、函数、变量等代码级别的重构,比如规范命名、规范注释、消除超大类或函数、提取重复代码等等。
小型重构更多的是利用我们能后面要讲到的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入 bug 的风险相对来说也会比较小。你只需要熟练掌握各种编码规范,就可以做到得心应手。
2.2 何时重构
需要说明的问题
个人比较反对,平时不注重代码质量,堆砌烂代码,实在维护不了了就大刀阔斧地重构、甚至重写的行为。
有时候项目代码太多了,重构很难做得彻底,这就更麻烦了!所以,寄希望于在代码烂到一定程度之后,集中重构解决所有问题是不现实的,我们必须探索一条可持续、可演进的方式。
找到代码中的问题
可以看看项目中有哪些写得不够好的、可以优化的代码,主动去重构一下。或者,在修改、添加某个功能代码的时候,你也可以顺手把不符合编码规范、不好的设计重构一下。
2.3 思考如何重构
进行大型重构的时候
要提前做好完善的重构计划,有条不紊地分阶段来进行。每个阶段完成一小部分代码的重构,然后提交、测试、运行,发现没有问题之后,再继续进行下一阶段的重构,保证代码仓库中的代码一直处于可运行、逻辑正确的状态。
每个阶段,我们都要控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要的时候还需要写一些兼容过渡代码。
小规模低层次的重构
因为影响范围小,改动耗时短,所以,随时都可以去做。按照分,拆的思想不断优化代码。
借助工具分析代码问题重构
除了人工去发现低层次的质量问题,还可以借助很多成熟的静态代码分析工具(比如 FindBugs、PMD),来自动发现代码中的问题,然后针对性地进行重构优化。
03.重构技术手段
如何保证重构不出错呢?
你需要熟练掌握各种设计原则、思想、模式,还需要对所重构的业务和代码有足够的了解。除了这些个人能力因素之外,最可落地执行、最有效的保证重构不出错的手段应该就是单元测试(Unit Testing)了。
当重构完成之后,如果新的代码仍然能通过单元测试,那就说明代码原有逻辑的正确性未被破坏,原有的外部可见行为未变。
3.0 举一个重构例子
举一个简单的例子看重构事项
现状:App 业务线各种依赖庞大,然后交错在一起,关系变的复杂。依赖库之间的强依赖导致版本冲突多。模块方案发生变化,上层修改成本大。功能模块兼容性导致维护成本大。
重构方案:整个架构的核心思想是面向接口编程和依赖注入使各个模块之间实现解耦,然后通过横向角色划分与纵向层级划分的方式约定各个模块之间的关系,再通过接口分层的方式,明确具体模块在不同层级上需要实现的功能
3.1 罗列重构事项
针对重构的业务详细罗列
针对一个比较大的重构业务,先进行梳理,然后根据问题或痛点思考,罗列解决方案,然后开始实践并保证代码稳定性,最后测试并交付。
罗列重构事项
第一步:面向接口编程,根据业务抽取抽象接口,由于接口是对某个功能需求抽象,所以不会对具体的实现形成依赖。
第二步:层级划分与角色划分,总体分为三个层次:底层、组件层和应用层。
第三步:除了通过接口实现模块间的通讯方式,我们还设计了一套内部通讯协议,用于在应用内部消息通讯。对于一些易变的、灵活的、简单的通讯,可以直接通过发送消息的方式进行通讯。
第四步:抽离功能模块,抽离公共视图模块,抽离公共业务模块,抽离产线业务模块。
3.2 把握关键节点
比如针对该例子关键点
第一个关键点:依赖错综复杂,那么就需要划分层次图,将 App 的架构设计分层。比如基础层,组件层,服务层,业务层,壳工程。然后定义好从上到下的关系,这样依赖就清晰明了!
第二个关键点:上层修改成本高,那这个时候能够根据业务抽象一套交互的接口。
第三个关键点:代码的结构层次情绪,视图层,数据处理层,业务层等层次清晰。低耦合!
3.3 编写测试用例
编写详细的测试用例
可以模仿谷歌官方 demo 来编写测试用例,这个测试用例的测试粒度越小越好。首先保证主流程畅通,然后在写边界测试用例。
要把持续重构作为开发的一部分来执行
那写单元测试实际上就是落地执行持续重构的一个有效途径。设计和实现代码的时候,我们很难把所有的问题都想清楚。
而编写单元测试就相当于对代码的一次自我 Code Review,在这个过程中,可以发现一些设计上的问题(比如代码设计的不可测试)以及代码编写方面的问题(比如一些边界条件处理不当)等,然后针对性的进行重构。
3.4 mock 业务数据
mock 业务数据很重要
尤其是有接口请求的业务逻辑,这个时候可以 mock 本地 json 数据,然后模拟各种业务场景。十分有利于调试各种 case
3.5 发现代码 bug
积极发现代码 bug 很重要
在代码演进中,bug 避免重复出现很重要。作为实践写代码,这个过程自测 case 罗列一下,然后自测,自测一项勾选一项。
3.6 优化编码方式
了解敌人——丑陋的代码
臃肿的类:开发者不去思考这些功能是不是应该放在这同一个类中,导致这些类会变得很臃肿,造成一个类几千行,让下一个接盘侠欲哭无泪。
臃肿的方法:好几十上百行的一个函数堆在一块,用面向过程的思想来写代码,建议一个方法代码行数不超过 80。
函数参数过多:函数参数过多会导致调用者对方法难以理解,参数弄混。建议可以将参数组成一个对象传入。
层层嵌套的判断:如果逻辑不复杂尽量减少 if-else 的分支包裹,太难阅读。比如不满足条件了直接 return,不走其他代码,这样可以减少一层嵌套。
满篇跑常量值:一个类里面出现各种未命名的常量值。0,1,2 等等铺天盖地。这种状态码意义改了,改代码会把你改哭的。难道就不能先声明一个统一的常量变量来使用吗。
模棱两可的命名:不能根据名字一眼看懂它的功能的命名不是一个好命名。当然生僻的单词除外。模糊的,没有功能意义的命名会给阅读造成很大困难。
优化编码的具体操作
分拆大函数:当函数比较大了,就可以根据功能节点分拆成多个小函数,也许其中的小函数还可以公用。比如结算购物车,包括计算各类商品的总价,再计算折扣,再计算满减优惠。分别拆分成三个,一眼就能看出这段逻辑先后做了什么。
封装到父类:如果多各类要执行相似的功能和代码,可以把该方法放到它们的父类中,或者提取出来成业务工具类。
针对类臃肿:方法迁移,遵守“单一职责”原则,当类中的方法不适合放在当前类中时,就应该为该方法寻找合适下家。移到与方法耦合大的类中。
搬移字段:当在一个类中的某一个字段,被另一个类的对象频繁使用时,我们就应该考虑将这个字段的位置进行更改了
提炼类:一个类如果过于复杂,做了好多的事情,违背了“单一职责”的原则,所以需要将其可以独立的模块进行拆分,当然有可能由一个类拆分出多个类。对类细化。
提升方法、字段:将方法向继承链上层迁移的过程。用于一个方法被多个实现者使用时。在继承的体系中,当多个类使用了相同或类似的方法,就可以考虑将该方法抽取到基类,没有基类就创建一个。字段提升同方法。
降低方法:即父类抽象方法让多个子类实现。多个子类有相同的功能但是有各个具体的实现方法,那么这种封装就可以用多态性了,父类创建一个抽象方法,将方法实现降低到子类。
重复代码的提炼:有时候为了赶项目进度,尽快完成功能,会偷懒将实现功能的一片代码复制一遍,直接套用。这种把多余的删掉,保留一个,也许只需传一两个参数就可以封成一个方法供多处调用。
重命名变量(类、方法、变量):这个很重要,可以不夸张地说,命名的水平就体现了编程能力的高低。在重构的过程中,当发现类名,方法名在当前版本不符合它的功能含义,就该考虑对其重新命名。
补加注释:对于全局变量,公用函数,逻辑复杂的地方添加注释,弥补之前的遗漏。
04.避免重构失败
4.1 能否给充足理由
为何要这样重构
如果自己很难讲清楚,或者给出的理由都比较牵强,没有压倒性的优势,那基本上就可以断定这是一种过度设计,是为了设计而设计。
4.2 乱套设计模式
不假思索地套用学过的设计模式
看到某个场景之后,觉得跟之前在某本书中看到的某个设计模式的应用场景很相似,就套用上去,也不考虑到底合不合适。
最后如果有人问起了,就再找几个不痛不痒、很不具体的伪需求来搪塞,比如提高了代码的扩展性、满足了开闭原则等等。
4.3 先有问题后改造
先遇到问题然后再改造
从问题讲起,一步一步给你展示为什么要用某个设计模式,而不是一开始就告诉你最终的设计。
看到某段代码之后能分析
就能够自己分析得头头是道,说出它好的地方、不好的地方,为什么好、为什么不好,不好的如何改善,可以应用哪种设计模式,应用了之后有哪些副作用要控制等等。
05.架构设计思考
5.1 针对复杂场景
设计模式要干的事情就是解耦
也就是利用更好的代码结构将一大坨代码拆分成职责更单一的小类,让其满足高内聚低耦合等特性。
创建型模式是将创建和使用代码解耦,结构型模式是将不同的功能代码解耦,行为型模式是将不同的行为代码解耦。而解耦的主要目的是应对代码的复杂性。设计模式就是为了解决复杂代码问题而产生的。
根据是否复杂投入时间
对于复杂代码,比如项目代码量多、开发周期长、参与开发的人员多,我们前期要多花点时间在设计上,越是复杂代码,花在设计上的时间就要越多。
如果你参与的只是一个简单的项目,代码量不多,开发人员也不多,那简单的问题用简单的解决方案就好,不要引入过于复杂的设计模式,将简单问题复杂化。
5.2 如何做架构设计
架构设计要以实用为目的
不要光想着造一个世上最牛逼的架构,这样往往是不靠谱的,我们不是救世主。一切都是当前实际情况为主!
总结下,架构设计有三个基本原则:
1、合适优于世界领先。适合自己当前业务的就好,不要总想搞领先的架构,比如一个用户量 100 万的 App,光想着对标微信的架构,适合微信的架构未必适合自己。
2、简单优于复杂。如同写代码一样,代码量越少越简单越好,架构设计也是一样,越简单的架构越容易看懂和维护。
3、演进优于一步到位。可扩展性我们当然要考虑,但是人不是神,无论你怎么去预测未来的系统演进,总是很大可能会失算。所以架构设计优先解决当下的问题,至于后来的问题,到时候再对架构方案进行改进。
这三个原则也是有优先级的
具体是:合适优于先进 > 演化优于一步到位 > 简单优于复杂
合适也就是适应当前需要是首位的,连当前需求都满足不了谈不到其他。架构整体发展是要不断演进的,在这个大前提下,尽量追求简单,但也有该复杂的时候,就要复杂,比如生物从单细胞一直演化到如今,复杂是避免不了的。
评论