1. 遗留项目中的挑战
1. 遗留项目的定义
定义:任何已经存在的、难以维护的、或难以扩展的项目。
特征:
- 老旧:经历几年的时间
- 庞大:项目越大越难维护。需要理解的代码越多,存在的bug越多,回归问题的可能性越大。
- 人员流失:开发功能的人和维护的人不是同一批。
- 文档不完善:没有文档、文档不全、文档失真
2. 遗留代码
- 没有测试或者无法测试:测试是摸索系统行为和设计的入口。
- 不灵活的代码:实现新功能或者修改现有的行为会非常困难,要涉及很多地方的代码。
- 被技术债务拖累的代码:债务是质量问题的累积,会产生利息,让你编码困难,甚至阻止开发工作。
3. 遗留基础设施
- 开发环境:从0开始搭建开发环境的时间。
- 过时的依赖:升级依赖常常会提供性能改善和bug修复。
- 异构环境:开发、测试、生产环境不一致。导致只有生产环境才有特定bug。
4. 遗留文化
- 害怕变化
操作的分析
| 更改 | 收益 | 风险 | 需要采取的行动 |
|---|---|---|---|
| 删除一个旧特性 | 更容易开发;更好的性能 | 还有用户使用这个特性 | 检查访问日志;询问用户 |
| 重构 | 更容易开发 | 回归问题 | 代码评审;测试 |
| 升级类库 | 修复bug;提升性能 | 类库行为的改变带来的回归问题 | 阅读变更日志, 评审类库代码, 手工测试主要特性 |
- 知识仓库
需要的知识:
- 用户需求和软件功能规范化相关的领域信息
- 关于软件的设计、架构和内部的项目特定的技术信息
- 通用的技术知识
解决方式:努力培养沟通和信息分享的环境,避免成为领域知识孤岛
原因:
- 缺乏面对面沟通
- 代码是我的
- 忙碌的面孔
解决方式:代码评审、结对编程、黑客马拉松
2. 找到起点
1. 克服恐惧和沮丧
对代码变更的恐惧是对未知的恐惧。
解决方式:
- 探索性重构:深入到代码中并开始使用它。目标是类和方法。
- 工具的帮助
- 版本控制工具:SVN、GIT提供的回滚。
- IDE:REFACTOR(重构)栏的功能。
- 编译器:每次重构后,进行编译,能检查引入的编译错误。
- 其它开发人员:为了减少犯错,让其它同事评审你的更改。(结对编程)
- 特征测试:验证 系统指定部件当前行为的测试。目标是描述系统的实际行为。
沮丧:失去动力和孤注一掷。
解决方案:确保重构上的努力是有作用的。可以选择一个或多个表示代码质量的指标来做到这一点。
- 显示软件的质量以及质量是如何随时间而变化的
- 决定我们的下一个重构目标应该是什么。
2. 搜集软件的有用数据
搜集的原因:
开始时代码是什么状态?他是否真的如你想想的糟糕
在任何给定的时间,你的下一个重构目标是什么
你的重构有什么进展
bug和编码标准违例
FindBugs、PMD和Checkstyle性能
生产请求错误计数
对常见任务计时
- 搭建开发环境的时间
- 发布或部署项目的时间
- 修复一个bug的平均时间
- 常用类文件
3. 用FindBugs、PMD和Checkstyle审查代码库
FindBugs:在Java代码中发现潜在的bug。
PMD:根据规则查找问题代码的工具。
Checkstyle: 确保所有的源代码遵循你团队的编码标准。
使用顺序:FindBugs->PMD->CheckStyle
分别减少bug、提高设计、提高可读性。
使用方式:IDE中装对应插件
4. 用Jekins进行持续审查
每当开发人员检入新代码时,构建服务器就自动运行审查工具,并在仪表盘上显示结果,以便团队成员在他们空闲时间查看。
提交代码-》触发构建-》构建项目并运行审查工具-》给出反馈-》检查项目状态
SonarQube:跟踪和可视化代码质量的独立服务器。
3. 准备重构
重构时要始终记得组织目标。
1. 达成团队共识
整个团队应该共同进行代码更改、评审彼此工作并分享从重构中获取的信息。
方案:
- 结对编程:一人引导(范围、方法、设计),一人实施
- 解释技术债务
- 代码评审:没有通过代码评审之前,更改都不能合并到主分支。
- 自动化测试:所有的改动必须有自动化测试覆盖。测试代码和生产代码一样需要评审。
- 划定代码区域:决定是否重构、如何重构,以及对应的价值
- 定期研究会(比如两周一次的技术分享)
2. 获得组织的批准
重构是保留系统的现有行为,不会增加任何新的功能,甚至不会修复bug。
如果对遗留系统进行重大的改进,甚至是完全重写,你需要专门的时间和资源。
3. 选择重构目标
三个维度:价值、难度、风险。
建议:
- 容易实现的目标(风险低、难度低):可以作为起点
- 痛点(价值高)
4. 决策时间:重构还是重写
不应该重写的情况:
- 风险:可能需要几个月,在完成之前一直不可用,回归问题,半途终止代价大。
- 开销:文档、代码
- 任务总是超出预期时间
好处: - 自由:自由改掉一些本来没法修改的代码
- 可测试性:从一开始就把可测试性放到设计中。
重写的必要条件:
- 尝试过重构并且失败了
- 编程范型的转型
增量式重写:将重写分成若干较小的阶段,但每个阶段都应该提供业务价值,应该可以在任何给定阶段之后停止项目,并且仍然能获得一些好处。
方法:将遗留软件分成多个逻辑组件,然后一次重写一个。
示例:
| 阶段 | 描述 | 业务价值 |
|---|---|---|
| 0 | 初步重构。定义组件接口,将组件拆分 | 清晰的接口能增加代码的可维护性 |
| 1 | 重写身份证组件。更改密码的存储方式 | 更好的遵守数据安全规定 |
| 2 | 重写搜索组件。切换到不同的搜索引擎实现 | 搜索结果质量更好。用户更容易找到产品 |
绞杀者模式和增量重写很类似。
4. 重构
1. 有纪律的重构
- 依靠IDE
IDE的REFACTOR功能特点: 更快、更安全、 更全面、更高效 - 版本控制工具
可以自由的退出某次重构 - Mikado方法
构建一个依赖图,里面包含了你需要执行的所有任务,用来参考调整顺序。
2. 常见的遗留代码的特征和重构
- 陈旧的代码
已经不再需要的代码。
移除陈旧代码的好处:
- 需要阅读的代码更少了
- 减少了任务浪费时间去修复或重构已经不再使用的代码的机会
- 增加项目的测试覆盖率
类别: - 被注释掉的代码
- 绝对不会被执行的代码
- 不再被其它模块使用的代码
违反规范
如类名、数据库设计较弱的测试
- 没有测试任何东西的测试
- 脆弱的测试:单元测试过于细密,重构代码时经常要修改测试代码。
- 错综复杂的业务逻辑
业务规则自身复杂或者与其他处理交织在一起。
解决方法:设计模式(责任链、装饰者、策略、状态)
3. 测试遗留代码
在重构遗留代码时,自动化测试能有效的保证重构不会在无意中影响软件的行为。
- 增加单元测试
- 单元测试不是万能的(方法-》单元测试,模块-》功能测试,组件-》集成测试,系统-》系统测试)
- 别过度追求测试覆盖率(70左右)
- 自动化所有测试
流程的顺序:结对编程、代码评审、单元测试、功能测试、集成测试、系统测试、UI测试、性能测试、负载测试、冒烟测试、模糊测试等候,还要经过用户测试(渐进式发布新版本、收集真实用户数据、执行新版本的隐藏发布)
5. 重新架构
1. 什么是重搭架构
重新架构比方法和类的级别更高。重构是将一些类移动到单独的包中,重新架构是要从主代码库移动到从代码库。
将应用程序拆分为组件或成熟服务的主要目标:
- 通过模块化内建质量
- 良好的设计保障可维护性
- 通过独立达到自治
2. 将单体应用分级为模块
- 起点
画出模块化之前的程序图例 - 背景
重构的原因 - 项目目标
确定在项目完成时期望的状态。
- 引入显示接口。模块之间只能通过接口交互,方便mock实现。
- 将源代码拆分为模块。使容易使用,依赖关系明确。
- 改善依赖管理。
- 清理并简化构建脚本。分模块后,构建更复杂了。
慎重的目标: - 系统架构的变化
- 功能更改
- 定义模块和接口
画出期望的模块 - 构建脚本和依赖管理
maven - 分拆模块
模块框架就位后,定义接口并将源代码移动到模块中就相对容易。从最简单的模块开始。对于复杂的模块,使用依赖分析工具来定位这循环依赖,要经过多次耐心重构才可能达到目标。 - 结论
6. 大规模重写
1. 决定项目范围
- 项目目标是什么
- 黑盒式重写:保持功能与现在一直,但内部从头开始重新实现。可能是移植到新的技术栈或者让软件更容易维护。
- 温习式重写:使新软件的功能与旧的不同。
- 补偿式重写:开发一些新功能作为重写项目的一部分。
- 记录项目范围
项目范围文档中包括以下信息:
- 新功能(你要添加任何新功能吗?)
- 现有功能(你计划删除现有的软件功能吗?)
- 及时性与功能完整性(某个日期发布一个迭代)
- 分阶段发布(总结每个阶段的内容)
使用迭代方法,即做一些小的发布,并在每次发布中添加更多工鞥呢。
2. 从过去学习
3. 如何处理数据库
将新软件连接到现有数据库;创建爱你一个新数据库,然后做数据迁移
- 共享现有数据库
优点:简单(无数据迁移),无需更新其它应用程序和脚本
缺点:无法选择数据存储技术,无法重新架构,无法重构数据库表,有损坏数据的风险 - 创建新数据库
实时数据同步、批量同步
7. 停止编写遗留代码
1. 源代码并不是项目的全部
除了代码
- 技术因素:开发工具、自动化配置、持续集成,简化发布和部署流程
- 组织因素:文档、团队内部及团队之间的交流、软件质量文化、其它部门的压力
2. 信息不能是自由的
- 文档
编写、维护文档代价大。
有价值的文档:
- 信息丰富(做什么、如何做、为什么这么做)
- 易编写
- 易发现
- 易阅读
- 可信赖
应该定期评审自己的文档,并删除所有过时的文档。
- 促进沟通
- 代码评审
- 结对编程
- 技术访谈:展示他们的技术或者橙果,每个人都可以了解其他人在做什么
- 向其他团队展示你的项目:向其他团队介绍产品的技术概述。
- 黑客日:多个团队合作,使用新技术,构建很炫酷的东西。
3. 工作是做不完的
小洞不补,大洞吃苦。越早修复技术债务,就越容易修复。
- 代码评审
- 做笔记
- 开发人员引导,花几分钟介绍代码库,然后询问周围人的意见
- 评审要控制时间,可以多次评审
- 写出评审结果清单,给出具体行动或者想法,共享文档,几周后检查进展情况。
- 修复一扇窗户
如果一个破窗不修复,会有更多的破窗,混乱就会迅速增多。
4. 自动化一切
自动化测试、自动化构建、部署以及其他Jekins任务、自动化配置。
自动化的原因:减少错误、减少重复劳动。
5. 小型为佳
模块化是首要任务,保持代码库轻盈灵活。如果船有腐烂模板,将其替换,就能使船保留几个世纪。
7. 持续交付的软件系统架构
程序员的呐喊:
- 所有的团队都要以服务接口的方式,提供数据和各种功能
- 团队之间必须通过接口通信
- 不允许任何其他形式的操作:不允许直接读取其他团队的数据,不允许任何形式的后门。只能通过网络调用服务
- 具体实现技术不规定,Http、Pub\Sub等。
- 所有的服务接口,必须从一开始就一可以公开为设计导向
1. 大系统小做原则
- 持续交付架构要求
- 为测试而设计。能快速进入测试环节、方便测试
- 为部署而设计。降低部署花费的时间
- 为监控而设计。能对其监控,无需等客户反馈问题。
- 为扩展而设计。支持团队成员规模的扩展;支持系统自身的扩展。
- 为失效而设计。一旦部署或发布失败,如何优雅且快速的处理。
- 系统拆分原则
- 作为系统的一部分,每个组件或服务有清晰的业务职责,可以被独立的修改,甚至被替代
- 高内聚、低耦合,每个组件或服务只知道尽可能少的信息,完成相对独立的单一功能。
- 整个系统易于构建与测试。拆分后,这些组件仍然需要组合在一起,为用户提供服务。避免无法测试的情况。
- 使团队成员之间沟通协作更方便。
2. 常见架构模式
插件、单体、微服务
3. 架构改造实施模式
拆迁者、绞杀者、修缮者模式、数据库拆分。
拆迁者模式:根据当前业务需求,对软件架构重新设计,并组织单独团队,重新开发一个全新的版本,一次性完全替代原有的遗留系统。
好处:与旧系统无关。
缺点:
- 功能遗漏
- 无时间应对市场需求变化
- 人力资源消耗大。一部分人维护旧版本,一部分人进行重构
- 闭门造车:新版本上线后,无法满足业务需求。
绞杀者模式:保留原有遗留系统不变,通过不断构建新的服务,逐步使遗留系统失效,最终替代它。
好处:不会遗漏原有功能;可以稳定提供价值,频繁交付版本,方便监控改造进展。
坏处:架构改造时间过大;产生一定的迭代成本。
修缮者模式:将遗留系统的部分功能与其余部分隔离,以新的架构进行单独改善。改造只发生在同一个系统内部,而非遗留系统外部。
优点:系统外部无感知;不会遗漏原有需求;可以随时停下改造工作,响应高优先级的业务需求;避免“闭门造车”现象。
缺点:架构改造时间过长;会有额外的架构改造迭代成本。
数据库改造
数据库是单体应用的最大耦合点。参考如下步骤:
- 详细了解数据库结构
- 先拆分数据库,做数据库迁移
- 将拆分出的程序模块和数据库组合在一起,形成微服务。