1、 为什么需要重构
在互联网行业,每当新员工入职一家新公司时,都要学习一套新的软件系统。如果该公司的代码非常规范,架构设计非常合理,那么新员工上手的速度会非常快。当然,你这个螺丝钉的角色也就非常明显了。另一方面,如果面对『屎山』一样的祖传代码,就会有很多抱怨,学习起来也很痛苦。
从质量上,我把软件大致分为以下几种类型:
- 第一种:它们对稳定性、规范性要求非常高,所以代码中异常判断、校验非常多,代码看上去就很冗余,最典型代表是华为的电信级产品。
- 第二种:写得就很随意,风格各式各样,代表的例子是开源项目。
- 第三种:也是最糟糕的,员工在 PM(产品经理)的高压下日夜加班赶出来的,毫无设计风格,能跑就行,追求最快速度地实现。
重构应该也是互联网公司开发工作的一部分吧。当一个软件系统需要重构时,必然是因为以下某种原因:比如二次开发难度越来越大、新员工上手越来越困难,每个模块只有特定的人懂;面对日益增长的功能需求,现有架构已经满足不了;模块耦合非常严重,导致不同的开发团队之间互相依赖,严重阻碍了开发进度。
1.1 二次开发的难度越来越大、新员工上手成本越来越高
模块之间耦合严重,类间调用关系形成双向依赖。大量的 if/else 分支、运行流程分支太多,圈复杂度爆表,根本理不清。当我们想要增加一个新功能,即使这个功能很小很小,牵扯的链条也非常长,涉及需要改动的周边函数、文件数量巨多。
1.2 腐朽的软件架构无法满足日益增长的功能需求
软件系统从最初的 demo 开始,不断完善,一点点添加新功能,以适应不同的应用场景。比如滴滴这款软件,最初只要把司机端和乘客接进来就行,加上调度系统负责派单,用户规模也比较小;后来,用户规模陡增,需要扩展服务器数量,又牵扯到负载均衡、消息中间件、高并发等需求 ;再后来,添加各种服务类型,比如顺风车、专车、豪华车等等,然后又是各种红包、优惠券等。
1.3 模块耦合严重,无法上云
微服务的前提条件就是模块间能解耦,这不仅是上云的需求,也能提高研发团队整体的开发效率,更重要的是为了实现服务编排,可以给任意子的服务提供灵活的资源,从而最大化集群的资源利用率,也就是说能更好地做到弹性扩缩容和容错。
1.4 新功能一起考虑进去
传统概念中对代码重构的理解是『不引入任何新功能』。我的看法是,代码重构和新功能开发结合起来,这样更有利于最大化重构效果。
2、 重构设计的指导原则
重构也是软件架构设计的一种,这里我称之为『重构设计』。
首先,你要清楚重构的目标是什么。比如侧重满足二次开发,或者侧重模块解耦,或者兼容各种硬件平台、编程语言等等。
其次,你要对基本的软件架构和软件设计风格有清晰的了解,以下是一些必备技能:
2.1、unix 编程艺术
- 模块原则:使用简洁的接口拼合简单的部件
- 清晰原则:清晰胜于技巧
- 组合原则:设计时考虑拼接组合
- 分离原则:策略同机制分离,接口同引擎分离
- 简洁原则:设计要简洁,复杂度能低则低
- 吝啬原则:除非却无他法,不要编写庞大的程序
- 透明性原则:设计要可见,以便审查和调试
- 健壮原则:健壮源于透明与简洁
- 表示原则:把知识叠入数据以求逻辑质朴而健壮
- 通俗原则:接口设计避免标新立异
- 缄默原则:如果一个程序没什么好说的,就沉默
- 补救原则:出现异常时,马上退出并给出足够多的错误信息
- 经济原则:宁花机器一分,不花程序员一秒
- 生成原则:避免手工hack,尽量编写程序去生成程序
- 优化原则:雕琢前要先有圆形,跑之前先学会走
- 多样性原则:绝不相信所谓”不二法门“的断言
- 扩展原则:设计着眼未来,未来总比预想来得快
2.2、 设计模式 + SOLID
23 种设计模式,你不一定要完全了解代码怎么写,但一定要知道每一种设计模式背后的设计思想是什么。有一段时间,我试图在我的代码应用各种设计模式,可最终代码看起来特别冗余而且不是那么必要。从个人经验上来讲,平时业务代码中用设计模式的场合非常少,最常用的无非是工厂、适配器、责任链等,而且效果并没那么大,设计模式真正适合的场合是更高层级的,比如模块间设计等等。
单一职责、开闭原则、里氏替换、迪米特法则、接口隔离、依赖倒置。
2.3、领域驱动设计(DDD)
有了解过一些,没有亲身实践过。它大体上就是模块解耦和分层的思想:API(对外访问层)、Domain(领域层)、Repository(数据源访问代理层)及基础设施层(DB、Redis、HTTP、RPC 等)
2.4、逐行代码的重构方法
参考马丁.福勒那本经典的《重构:改善代码的设计》,比如过长的函数、过长的参数、数据泥团怎么处理等等。
这里,也说下个人的小建议:比如纵向的调用关系变为横向的,减少函数调用栈深度;不要过度封装。相信用户能找到底层类的实现接口;逻辑上相关的代码物理上尽可能放在一块;对于某个小的具体功能,涉及的链条越短越好;面向接口编程;访问 A 表数据的 class 中不能存在访问 B 表数据的 function;模块对外暴露的接口部分,数据类型的选择上尽量做到宽进严出(接口要考虑通用性);写操作接口,接收参数尽可能少;读操作接口,返回参数尽可能多;减少不必要的类和数据结构等等。
2.5、微服务
2.6、云原生
毫无疑问,云是未来数字世界的基础设施。
2.7、插件思想
比如约定/注入插件、事件插件、插槽插件等等。
好了,这里不再列举了,因为根据哈弗大学心理学博士米勒的研究,对于每一类产品,用户最多只能记住七类品牌,这里我也就列七个重构原则~
3、 如何撰写重构设计文档
以面向对象语言为例,这里我把它分为了几个步骤:
- 旧系统的类间调用关系图 vs 新系统的类间调用关系图
- 旧系统的整体架构图 vs 新系统的整体架构图
- 旧系统的运行流程图 vs 新系统的运行流程图
- 分模块逐一拆解:先画出重构前的样子,再画出重构后的样子
- 新增功能有哪些,怎么在重构后的系统里添加
- 考虑对旧系统的影响:兼容性问题
- 考虑实施可行性: 重构成本和收益之间如何权衡
4、重构的具体执行步骤
总体上有两种方式:
- 新建分支(不推荐)
- 新建目录(推荐),这样能保证原来旧的那套代码能用。这里又有两种方式:
- 做减法(先跑通旧的代码,然后一点点删除冗余代码,或重构具体子模块)--推荐,因为如果实在没有开发时间了,某些未完成的模块还可以沿用以前的老代码。
- 做加法(从每个子功能开始重构,相当于重新构建这套软件系统)-- 不推荐,因为时间成本不可控
5、总结
用设计原则和架构理论武装自己,阅读优秀的源码验证理论,深入理解具体业务,方能设计出一套优雅的软件系统。