by zhimaxingzhe from 如何接手别人的系统-遗留系统重建之道法术器势志 欢迎分享链接,转载请注明出处,尊重版权,若急用请联系授权。 https://zhimaxingzhe.github.io
前言
成熟的公司会有大量的存量系统,程序员难免接手他人开的的系统。万一不小心接手的系统过于腐烂,祖传代码难以破译,一边吃力不讨好艰难维护老系统,一边在上面做新业务,出了问题要背大锅,一头包,难有好成绩,满身疲惫,终成大冤种。本文尝试探讨如何接手遗留系统的方法论,重建遗留系统的道法术器势志,使得遗留系统跟上组织内系统演进和满足业务需求,逐步从泥沼中走脱。
什么是遗留系统?接手别人开发的系统,可以是各种原因导致的系统建设、维护工作的交接,对于接手人来说就属于是遗留系统;严格意义上来说任何已存在的系统都可以是遗留系统。
接手一个遗留的系统并非一定要重建它,如果是一个即将告别历史舞台的系统,我们没有必要讨论,只需要把它“送好最后一程”即可。本文讨论的范畴是遗留系统将要长期服役下去,并且会在上面发展更多业务,长出新功能的系统,也就是图中将遗留系统变成一个现代化的系统。
那么首先,在拿到交接的材料中基本了解系统后,我们从业务需求、系统功能、系统框架,特别是对系统设计有了整体的认识之后,我们就应该思考一个问题,该系统是否符合现代系统的要求,技术上应该延续现有设计,还是重构、重写、重搭、迁移。如果是延续现有系统设计,则本文往下就没有必要再往下看了。
当你认为该系统实在是烂,交到你手里维护你就要做大冤种了,这个时候就应该思考重建该系统的方法(套路)了,最好能做到私人订制。如何能做到私人订制?我们从业界成熟方法论说起,再以笔者经验实战情况,总结梳理出能抡的三板斧。希望能帮助到各位,若有帮助,请一键三连。
以术载道,以道驭术,以法固道,善于驭器,勤于练术,术器生势,顺势成志。本文的内容较多,先来一览目录,源道法术器势志都包含哪些内容,随后再详细展开陈述。
一、源起-为什么要重建
首先要明确重构的动机,把动机详清楚,痛点在哪,拿出来和组内的同学一起讨论,切记要客观。是系统框架太老旧?还是系统架构难以为继?还是有很多坏代码的味道?还是因为系统太过复杂,没办法短时间内了解到全,出于处于懒惰,选择逃避?还是遗留系统太过腐烂,历经N手开发人员后,根本没有人员、文档、任何材料能支撑起你去认识该系统,导致你无法从旧系统中获得知识,所以想要尽快与遗留系统划清界限?
01、遗留风险
1.业务不能满足发展
从业务角度来说系统的能力无法支持业务发展的需要,功能设计落后于业务发展,需要改造现有功能的能力。
2.系统能力不能满足发展
如系统部署架构不能满足业务规模的发展,无法支撑用户量、访问量。
3.新需求交付时间长
在不了解的系统上做需求会遇上或多或少的未知情况,需要在不断踏坑、填坑中调整,完成任务的时间需要不断加长,交付时间常常比预估的要长。
4.缺陷多,到处救火
团队长期处于救火状态,每天白天做消防员,晚上做施工员。不管是业务团队还是技术团队都处于较大压力的工作环境中,长期以往精神状态会受到影响,不利于团队建设。
5.交付周期不可控,业务无法按期开展
由于第3、第4点导致交付工作的投入不足,需要延长排期计划,无法及时上线新需求,新业务也无法按计划开展。
02、研发价值
1.研发成本高居不下
这个显而易见,不需要解释了吧。
2.架构落后陈旧
如应用是单体的,需要集群化、微服务化、分布式化。
3.模块设计不合理
模块间边界不清晰,开发人员不能很好识别模块分层,不能正确安排类的存放路径,导致模块边界越来越不清晰,远离开闭原则。如采用MVC架构,没有严格按照MVC模块划分原则,controller 调用 dao的情况;采用DDD架构,domain之间有互相调用的情况等。
4.模式设计混乱
模式设计乱用,没能结合业务来正确使用设计模式,导致接口拓展矛盾重重。
5.代码腐烂
代码年久失修,堆积成山。
6.新增、修改代码困难
N代的祖传代码,每个开发人员都按照自己的意图添加代码,远离单一职责,接手人想要添加一个简单修改,却需要同时考虑N多种情况。
7.没有足够的测试
遗留代码没有写测试用例,没有用例代码,代码不可测试,或者没有足够的测试用例验证已有功能的正确性。
8.没有足够的知识(人/文档)
团队中找不到对系统足够了解的人,可能此前的开发团队成员全都离开了,或者只有少数不了解全局情况的人员留下来了,或者组织架构调整,系统没有跟随人员变动调整,全都交给了新的团队来负责。没有跟上系统现状的文档,没有可以了解系统现状情况的文档,甚至没有文档。
9.难以定制让人放心的交付计划
对于不了解的系统,始终会有坑在前头,计划赶不上变化,何况这些坑就不是计划的一部分。
10.界定修改带来的影响
A: 一处小修改,把系统搞垮了?修改前怎么不评估影响?
B: 我不知道这点修改会影响到另一个模块,没有文档没有注释,我甚至不知道那个核心模块有依赖这里。
A: 没做集成测试?
B: 只改了这点,单测没问题,在这个模块范围下也是没有任何问题的,所以没有做全局的测试。
A: ......
11.难以控制交付风险
既然不知道影响,那就更不知道交付出去的产物是不是可靠的,如果经常出现问题,会打击团队士气,开发人员信心会丧失,也许整天都在担心线上跑的代码会出问题,变成祈祷式开发,神学运维,玄学运营。
03、业务价值
让业务能够健康发展,或者可以重新设计系统功能。不被技术债拖累,不被陈旧技术限制发展,有效可靠的交付周期,更可靠更现代化的业务能力,用户体验更好。
04、重建风险
重构若不做好风险控制会带来难以控制的风险的情况下。需要列出风险TODOLIST,盯紧风险因素在重构过程中的处理情况。
还有一种特殊的风险是重写后的代码与原来的非常相似,因为要考虑复杂的需求场景,为了避免这一问题,就要从旧系统汲取到足够的知识,来判断该功能的实现是否河里了。这里除了从阅读代码、查找文档之外,最好的方式是从构建测试开始,根据测试来了解功能\接口的行为。
1.工作量超预期
重建本身是复杂的工作,需要根据系统因地制宜设计方案,遇到意料之外的情况也是常态,工作量超出预期就显而易见了。
2.卡住业务发展
无法按期交付,当前重构带来的变化使得系统版本没办法交付业务需求。因为不能在重构的版本上做业务需求的开发,如果在重构前的版本上开发业务需求,又会导致业务代码在交付后还要合并到重构版本上,带来联调、测试、验收、验证等工作量;如此一来,团队取舍困难,从而可能卡住业务发展。
3.逐步变成老系统的样子
有的时候看到整个模块十分复杂,包容了非常多的情况,逻辑点非常多,接口代码冗长不堪,开始着手重构,但越重构越发现新的实现越来越接近老系统的实现,因为要实现的业务场景本身就是很复杂的。
4.切换后回退困难
重建带来的修改,在研发的各个阶段都可能导致回退,一些较高层面的重构回退的成本也会升高,导致回退难度大。如将系统架构重新规划,原本处于特定业务领域的服务应当沉淀为基础服务,在发布后发现实际效果带来更多问题,还不如原来的方式处理来的简单;又如将模块重新划分,将部分接口实现迁移到更内聚的模块中,之后又做了接口代码的修改,添加了许多类,在对实际情况更加了解后,认为这些接口归属在原来的模块下是更加正确的;此时想要回退就会比较麻烦。如修改了代码的目录结构后,又修改了接口代码,此时想要保留接口代码的修改,还原代码目录结构就不能通过退回来实现。
还有更麻烦的奖数据库重建后,新库在线上运行遇到较多问题,此时如果没有做好退回的方案设计,将会陷入两难境地。
05、重建成本
软件开发总成本 = 开发成本 + 维护成本;软件维护成本 = 理解成本 + 修改成本 + 测试成本 + 部署成本。若是没有梳理重构成本,会导致任务总是超出预期时间。
包括以下方面的工作量:
1.梳理工作量
2.设计工作量
3.开发工作量
4.测试工作量
5.切换工作量
06、重建收益
1.降低研发成本
2.系统架构现代化
3.模块设计更科学
4.模式设计更科学
5.代码新生
6.充足的测试用例
7.贴合实现的文档
8.提高交付效率和质量
9、可测试性,TDD方式
如果是重写的方式来重建,还有以下好处:
1.不受原来包袱的限制
2.编程规范的转变
二、道-怎么重建
01、系统的声明周期和发展规律
系统本身会发生交替更迭,新系统会替代老系统,
当然在任何阶段都有可能发生交接,对于新接手的同事来说都是遗留系统。
02、研发流程周期
敏捷开发时代下,我们需要经历需求、计划、编码、构建、单元测试、功能测试、系统测试、发布、运维、监控等阶段,有可能一个团队下在并行几个devops环,虽然不提倡,但是这是很多程序员正在经历的事实。
在结合系统声明周期和研发流程周期来看,我们需要怎么样的重建流程呢?
03、重建流程
PDCA是由于爱德华兹·戴明博士而出名的,很多人认为戴明博士是现代产品品质控制的始祖,分别指规划、执行、检查、行动/修正、总结。
我在前后加了评估、总结,评估是为了确认是否有重构的必要,评估复杂度、影响范围以及工作量得出重构成本,从重构收益和风险。
04、重建方案选择
因材施教、因地制宜,对症下药,视系统情况而选择不同层面的重建方案。
重构
将现有系统在不改变软件的外部行为的基础上,改变软件内部的结构,使其更加易于阅读、易于维护和易于变更。
重写
重头开始写一个全新的系统来替代现有系统。
重搭
重搭也属于重写的一种,只不过偏向于因为系统架构不合理导致的重搭重建,内部业务功能的实现并不需要大改,大部分是迁移和整合。
迁移
当遗留系统是可以合并到一个现代化的系统当中时采取的方案,通常和重搭的后半部分工作是一致的,内部业务功能的实现并不需要大改,大部分是迁移和整合。
最佳实践方式:持续性重建
以上描述的每一种方法在时机执行时都会被“同时要支持业务发展”打脸,我们几乎做不到完整地执行重构、重写、重搭、迁移的同时丝毫不耽误业务的发展,不产生线上问题,为了避免这些大动作导致系统陷入不可用、缺陷过多等尴尬境地,我们不能把步子迈得太大,这样才不会扯到这个腐烂的蛋,再出现问题时至少还有快速回到上一步的可能。当然你可能会想,回滚系统发布不就好了么?但是,回滚工作量和系统现代化步长是成正比的,系统陷入泥潭的概率会增加,步长越大系统风险也就越大。
既然如此,我们只能是增量重建,在每个重建阶段都应该提供业务价值,跟上业务发展,应该可以在任何重建阶段之后都能获得一些重建带来的好处。
所以我们看到 Java 界最会总结的程序员 Martin Fowler 推荐的绞杀模式是一种最佳实践。
绞杀者模式
三、法-重建方案设计和流程
1.达成团队共识
一切以业务为先,结合业务开展情况,适时结合起来规划重建任务。
2.获得组织的批准
与组织上下游做好足够的沟通,保证大家对遗留系统重建达成一致共识,组织应明白重建的预期,重建过程中遇到风险应尽早暴露风险,也更容易获得组织的理解和支持。
3.选择重构目标
寻找容易实现的目标(低风险、低难度)+ 痛点(价值高)来规划重构,以此开展的重构更能获得成就。
重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。—— 《重构:改善既有代码的设计》
4.鼓起勇气,不要害怕变化
当你试图对工作方式进行这些改进时,政治斗争可能抬起它丑陋的头——《拥抱变革:从优秀走向卓越的 48 个组织转型模式》
唯一不变的是变化,拥抱变化,成为变化的一份子。
决定项目范围
项目目标
通常重构都伴随着业务需求一同开展,人性本是无利不起早,业务需求和重构范围能正好重叠是最好的情况,但通常两者能有交集就挺不错的了,甚至这两个毫无交集,这种情况下我们更加要讲重构痛点,明确为什么要重构,怎么重构,重构后是怎么样的,能给团队、业务带来什么好处,双方沟通出一个目标清单。
记录项目范围
- 新功能
本次重构会带来哪些新功能,哪些是新增的业务需求。 - 现有功能
与本次重构范围相关的现有功能是哪一些,分析重构将会对这些功能造成影响,需要重点测试和验收。分析本身会对重构范围进行审视,如不能控制重构带来的风险,需要重新回到为什么要重构这部分内容当中去,是否重构的方案本身是不合理的,是否应该放弃重构,或者选择重写、重搭、迁移等其他方案。 - 及时性与功能完整性
计划要在何时完成发布,里程碑的风险是哪一些?如果项目关键路径无法及时完成,能否在不影响功能完整性的情况下,具备调整功能实现范围的条件? - 是否可以拆分为更小的阶段
小步快跑是很好的工作方式,当前的项目范围是否可以拆分为更小的阶段来实现,小步重构,快速交付发布,这样如果遇到不可预见的风险时,可以有快速回退的办法来避免更大的问题。
从构建测试开始
重构是对已有的系统、功能、代码进行修改,保证现有系统功能可以正常运转是团队接受重构的前提,那么保证质量的办法就是测试。所以在《重构:改善现有代码的设计》中,在很多讨论重构的书籍、文章中,都有提到构建测试是重构的开始。具备完备的自动化测试能力,最好有良好的持续集成系统,在每次小步重构提交后,可以快速得知重构对系统整体影响的反馈。
这里也有一点需要说明是,最小可发布单元(模块)应该足够小,一个模块承担太多的功能,会导致自动化测试等评估重构的指标难以快速产出,拖慢重构的效率。可以想到如果只是修改了一个字段名,却要跑十几分钟的自动化测试才能知道重构的结果,那将会打击重构的积极性,程序员将难以保持对重构的耐心,无法将重构融入日常工作当中。
软件质量保证是重构之所以能被组织接受的根本,如Google 测试之道认为软件质量是先天就创建出来的,Jsones Capers 的测试左移认为在编码阶段发现缺陷的修复成本是最低的,我们应该在编码阶段做足够的测试,这样能够提升研效保证质量。
测试遗留代码
为遗留代码创建测试用例,覆盖遗留代码的测试,构建业务护城河。
测试不可测试的代码
在开始重构前,需要先有单元测试,有单元测试,就可以开始重构,找到突破口,逐步建立更多的测试,进而可以做更多的重构和重建工作。
更多使用mock、构造哑实现来避免测试过程中遇到的副作用。
常见的测试策略有
- 框架/模式库测试。xUnit,xMock,如 Java 语言里的 JUnit, Mockito;JavaScript 中的 Jest
- 端到端 API 测试。JMeter,Postman,Rest Assured,Karate
- UI 集成测试。Protractor
考虑到测试即文档,在实现实现的时候,会配合一些支持自然语言描述的框架,如: - 文档式测试,Gauge (主流语言),Concordion(Java)
- BDD 测试,Cucumber(主流语言)
- ATDD 测试,Robot Framework(Python 语言)
为了与运行客户端一配合,还需要有底层 API 来控制浏览器、客户端应用: - Appium。移动 APP 和桌面应用,支持主流语言
- Selenium。Web 浏览器,支持主流语言
- Puppeteer。Node.js API 操作 Chrome 浏览器
没有单元测试的回归测试
单元测试不是“银弹”, 别过度追求测试覆盖率,适可而止,否则容易导致开发人员从编写简单的测试开始,而不是命中重要的测试目标。
自动化所有测试,这不仅仅是提效,也是质量的保证,手工的测试误差往往很大,稳定性难以保证,效果也有限测试不可测试的代码。
让用户为你工作
结对编程、代码评审、单元测试、功能测试、集成测试、系统测试、UI测试、性能测试、负载测试、冒烟测试、模式测试,但依然无法包含所有用户使用的情形。
渐进式发布新版本,同时监控错误和回归问题。
收集真实的用户数据,并利用它来使你的测试更高效。
执行新版本的灰度测试。
让一切自动化
自动化基础设置、持续集成自动化、测试自动化等是效率和质量的保证,机器比人稳定,手工总会出错,重复的工作抽象、封装出来让机器去做,节约下来的时间可以做更多有意义的事情。同时,这也使得遗留系统成为容易接手的系统,重构后的遗留系统交接给接班人后不再会轻易变成遗留系统,这是终结遗留系统的利器。
不同层面的重构
架构重构
架构重构包括系统群架构、具体的某个服务架构、公共区域等。
DDD架构下,从业务领域、功能领域划分系统、划分服务,以及由此形成的公共区域。
某个服务下比如MVC,接口层、服务层、dao层,还有一些实体、适配器、DDD的domain层、工具。
创建通用的共享组件导致了一系列问题,比如耦合、协调难度和复杂度增加。复用与低耦合 ,本身存在一定的互斥关系。
模块重构
模块/组件是软件的部署单元,是整个软件系统在部署过程中可以独立部署的最小实体。 —— 《架构整洁之道》
Bob 大叔在书中提到了三个原则:
- 复用/发布等同原则(REP)。软件复用的最小粒度等同于其发布的最小粒度。合理、有效的包发布策略。
- 共同闭包原则(CCP)。我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同组件中。模块包满足开闭原则。
- 共同复用原则(CRP)。不要强迫一个组件的用户依赖他们不需要的东西。单一职责原则。
所谓模块化,就是力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互,把原本散弹式。
结合DDD的思想,我们把系统按照业务领域划分为各个模块,再有一些技术范畴的模块和一些提供公共能力的放到公共模块,以及一些跨领域间的组织(编排)模块。按这些规则,形成上下文边界,防止代码越界建设。
模型重构
架构元模型定义了模型中使用的概念和使用规则。 —— 《架构师修炼之道》
聚合行为
如果我们不创建模式,而直接开始编写代码,那么我们会收获一堆上帝类。但是,反过来,当我们有一堆上帝类的时候,那么我们就需要从类中把行为都抽取出来。
当我们的贫血模型,拥有了行为,就可以进一步构成富血模型,符合面向对象(OO)的思想。进一步的,我们可以从业务的角度来考虑这个问题,将充血模型改为领域模型。
类、状态、方法、bean、model等。
基于对于DDD的理解,在系统中要遵循严格的DO、DTO、VO、BO、DAO、Entity、POJO模型划分。
模式重构
使用设计模式重构遗留系统代码实现,常用的有设计模式,工厂模式、单例模式、策略模式、门面模式、装饰者模式、责任链模式、建造者模式等。
代码重构
函数不该有 100 行那么长,20 行封顶最佳。--《代码整洁之道》
结合Robert C Martin的观点,函数长度应该在4-20行之间了,对于复杂业务代码来说很难做到,否则就需要将步骤拆得很细,将调用链路设计得很长,这对于代码可读性来说也是个挑战。结合工程实践经验来看,我们通常会在紧迫的时间内完成指定任务,按时交付,所以我个人的要求是不超过80行。
大神们的建议是我们的方向指引,就好比马丁·福勒(Martin Fowler)在《重构》中的提到一个好的变量名、函数名应该要好到不需要任何注释,读者能明白代码的用途,就像是在阅读小说一样清晰地阅读代码。
我的个人经验来看,工程实践情况下,我们的项目、系统通常是经手N代的,前人的设计思路、实现过程中的妥协、为适应当时情况添加的处理分支、开发者没有继续遵循最初的设计模式等等复杂情况,在经历几任程序员的维护后,业务代码会变得无比复杂。特别是核心的业务代码,如果没有足够的注释和文档,没有人敢保证能够在新接手的系统上快速了解到足够有用的知识和信息,就敢拍胸脯承诺改造排期。
降低圈复杂度,重构手法参考《重构:改善现有代码的设计(第二版)》,加上现代的IDE都有自动重构能力,所要做的,就是掌握重构手法,不断练习,领悟重构手法应该如何施展。
把重构和业务需求分开提交
通常在开发业务需求时闻到怀代码的味道,脑子里高速评估一圈后认为是时候抹掉这股怪味了。这时候要停下来,决定好是重构后提交代码,还是先开发业务需求提交代码,或者反过来做,这样做的好处是能够利用好版本控制系统快速回到上一步,且能降低出错率。如果将两者混淆在一起,很容易导致大脑需要同时思考这两件事情,容易产生错误;且在代码评审时、在他人review 这段代码时也能快速明白修改意图。
四、术-重建中的方法
代码重建方法
掌握重构手法,不断练习,领悟重构手法应该如何施展。重构手法的学习参见经典《重构:改善现有代码的设计(第二版)》。
数据库重建方法
包含数据库的遗留软件的两种方法:共享现有数据库或者创建新数据库并迁移数据。
共享现有数据库
改造持久层,适配新数据库数据结构。如果原本持久层设计不合理,或者与服务层耦合较深,则改造起来会比较麻烦。
创建一个新的数据库
实时同步
业界成熟的工具如canel、maxwell、DataX、Sqoop等。
批量同步
可通过 ETL 工具批量复制,如Kettle、Streamset。
复制流量(双写)
有点是最终决定切换前要做好充足的验证,切换到新库后无法回退到上一步。
级联同步
这种方案优势是简单易实施,在业务上基本没有改造的成本;缺点是在切写的时候需要短暂的停止写入,对于业务来说是有损的,不过如果在业务低峰期来执行切写,可以将对业务的影响降至最低。
五、器-重建的工具
六、势与志
以术载道,以道驭术,以法固道,善于驭器,勤于练术,术器生势,顺势成志。
沉淀重构资产
重构项目发布后,坚持复盘总结,可以是个人的也可以是团队的,形成重构的文化;发现流程中存在的问题,以便在下次重构时优化流程,提高效率;沉淀出更多好用的工具,每次重构都将重复的工作交给机器,做到自动化,特别是测试自动化工具的改进;沉淀出有用的文档,不管是记录还是指南手册,还是功能说明、设计方案,都需要在还记得清楚的时候及时做好整理。
一些原则和Tips
- 小步前进,不论改动的大小,一旦变动的文件多了,如移包、重命名用得广泛的类等等,记得随时提交。
- 随时可用。如果不能保证随时可用,那就说不上是重构了。
- 停止编写遗留代码
- 源代码并不是项目的全部
- 沉淀文档
- 促进沟通
- 最大化开发团队内部、之间的交流
- 重建工作是做不完的,持续重构,减少技术债
- 定期的代码评审
- 提前浏览代码、并做笔记
- 由熟悉代码的人引导评审
- 写出评审结果,得出行动和事项
- 自动化一切可以自动化的东西
知易行难,道阻且长,任重道远,整套方法的落地执行还要不断修炼捶打。文中的许要点展开都是一个复杂的知识点,若要详细讨论还可以用很多篇文章才能阐述清楚,限于个人能力和精力,不再详细展开,比如数据库迁移,比如重构手法,还有测试左移,甚至道法器术势志本身也是高深的学问。笔者知识有限,认知局限,有不足和纰漏之处,还请各位不吝赐教,欢迎在评论交流。若有帮助,请一键三连。
参考资料:
《遗留系统重建实战》
《Refactoring: Improving the Design of Existing Code》
《Applied Software Measurement : Global Analysis of Productivity and Quality》
《重构:改善现有代码的设计(第二版)》