重构基本理论

1. 遗留项目中的挑战

1. 遗留项目的定义

定义:任何已经存在的、难以维护的、或难以扩展的项目。
特征:

  • 老旧:经历几年的时间
  • 庞大:项目越大越难维护。需要理解的代码越多,存在的bug越多,回归问题的可能性越大。
  • 人员流失:开发功能的人和维护的人不是同一批。
  • 文档不完善:没有文档、文档不全、文档失真

2. 遗留代码

  1. 没有测试或者无法测试:测试是摸索系统行为和设计的入口。
  2. 不灵活的代码:实现新功能或者修改现有的行为会非常困难,要涉及很多地方的代码。
  3. 被技术债务拖累的代码:债务是质量问题的累积,会产生利息,让你编码困难,甚至阻止开发工作。

3. 遗留基础设施

  1. 开发环境:从0开始搭建开发环境的时间。
  2. 过时的依赖:升级依赖常常会提供性能改善和bug修复。
  3. 异构环境:开发、测试、生产环境不一致。导致只有生产环境才有特定bug。

4. 遗留文化

  1. 害怕变化
    操作的分析
更改 收益 风险 需要采取的行动
删除一个旧特性 更容易开发;更好的性能 还有用户使用这个特性 检查访问日志;询问用户
重构 更容易开发 回归问题 代码评审;测试
升级类库 修复bug;提升性能 类库行为的改变带来的回归问题 阅读变更日志, 评审类库代码, 手工测试主要特性
  1. 知识仓库
    需要的知识:
  • 用户需求和软件功能规范化相关的领域信息
  • 关于软件的设计、架构和内部的项目特定的技术信息
  • 通用的技术知识

解决方式:努力培养沟通和信息分享的环境,避免成为领域知识孤岛

原因:

  • 缺乏面对面沟通
  • 代码是我的
  • 忙碌的面孔

解决方式:代码评审、结对编程、黑客马拉松

2. 找到起点

1. 克服恐惧和沮丧

对代码变更的恐惧是对未知的恐惧。
解决方式:

  • 探索性重构:深入到代码中并开始使用它。目标是类和方法。
  • 工具的帮助
    1. 版本控制工具:SVN、GIT提供的回滚。
    2. IDE:REFACTOR(重构)栏的功能。
    3. 编译器:每次重构后,进行编译,能检查引入的编译错误。
    4. 其它开发人员:为了减少犯错,让其它同事评审你的更改。(结对编程)
  • 特征测试:验证 系统指定部件当前行为的测试。目标是描述系统的实际行为。

沮丧:失去动力和孤注一掷。
解决方案:确保重构上的努力是有作用的。可以选择一个或多个表示代码质量的指标来做到这一点。

  • 显示软件的质量以及质量是如何随时间而变化的
  • 决定我们的下一个重构目标应该是什么。

2. 搜集软件的有用数据

搜集的原因:

  1. 开始时代码是什么状态?他是否真的如你想想的糟糕

  2. 在任何给定的时间,你的下一个重构目标是什么

  3. 你的重构有什么进展

  4. bug和编码标准违例
    FindBugs、PMD和Checkstyle

  5. 性能

  6. 生产请求错误计数

  7. 对常见任务计时

  • 搭建开发环境的时间
  • 发布或部署项目的时间
  • 修复一个bug的平均时间
  1. 常用类文件

3. 用FindBugs、PMD和Checkstyle审查代码库

FindBugs:在Java代码中发现潜在的bug。
PMD:根据规则查找问题代码的工具。
Checkstyle: 确保所有的源代码遵循你团队的编码标准。

使用顺序:FindBugs->PMD->CheckStyle
分别减少bug、提高设计、提高可读性。
使用方式:IDE中装对应插件

4. 用Jekins进行持续审查

每当开发人员检入新代码时,构建服务器就自动运行审查工具,并在仪表盘上显示结果,以便团队成员在他们空闲时间查看。

提交代码-》触发构建-》构建项目并运行审查工具-》给出反馈-》检查项目状态

SonarQube:跟踪和可视化代码质量的独立服务器。

3. 准备重构

重构时要始终记得组织目标。

1. 达成团队共识

整个团队应该共同进行代码更改、评审彼此工作并分享从重构中获取的信息。
方案:

  1. 结对编程:一人引导(范围、方法、设计),一人实施
  2. 解释技术债务
  3. 代码评审:没有通过代码评审之前,更改都不能合并到主分支。
  4. 自动化测试:所有的改动必须有自动化测试覆盖。测试代码和生产代码一样需要评审。
  5. 划定代码区域:决定是否重构、如何重构,以及对应的价值
  6. 定期研究会(比如两周一次的技术分享)

2. 获得组织的批准

重构是保留系统的现有行为,不会增加任何新的功能,甚至不会修复bug。
如果对遗留系统进行重大的改进,甚至是完全重写,你需要专门的时间和资源。

3. 选择重构目标

三个维度:价值、难度、风险。
建议:

  • 容易实现的目标(风险低、难度低):可以作为起点
  • 痛点(价值高)

4. 决策时间:重构还是重写

不应该重写的情况:

  1. 风险:可能需要几个月,在完成之前一直不可用,回归问题,半途终止代价大。
  2. 开销:文档、代码
  3. 任务总是超出预期时间
    好处:
  4. 自由:自由改掉一些本来没法修改的代码
  5. 可测试性:从一开始就把可测试性放到设计中。

重写的必要条件:

  1. 尝试过重构并且失败了
  2. 编程范型的转型

增量式重写:将重写分成若干较小的阶段,但每个阶段都应该提供业务价值,应该可以在任何给定阶段之后停止项目,并且仍然能获得一些好处。
方法:将遗留软件分成多个逻辑组件,然后一次重写一个。

示例:

阶段 描述 业务价值
0 初步重构。定义组件接口,将组件拆分 清晰的接口能增加代码的可维护性
1 重写身份证组件。更改密码的存储方式 更好的遵守数据安全规定
2 重写搜索组件。切换到不同的搜索引擎实现 搜索结果质量更好。用户更容易找到产品

绞杀者模式和增量重写很类似。

4. 重构

1. 有纪律的重构

  1. 依靠IDE
    IDE的REFACTOR功能特点: 更快、更安全、 更全面、更高效
  2. 版本控制工具
    可以自由的退出某次重构
  3. Mikado方法
    构建一个依赖图,里面包含了你需要执行的所有任务,用来参考调整顺序。

2. 常见的遗留代码的特征和重构

  1. 陈旧的代码
    已经不再需要的代码。
    移除陈旧代码的好处:
  • 需要阅读的代码更少了
  • 减少了任务浪费时间去修复或重构已经不再使用的代码的机会
  • 增加项目的测试覆盖率
    类别:
  • 被注释掉的代码
  • 绝对不会被执行的代码
  • 不再被其它模块使用的代码
  1. 违反规范
    如类名、数据库设计

  2. 较弱的测试

  • 没有测试任何东西的测试
  • 脆弱的测试:单元测试过于细密,重构代码时经常要修改测试代码。
  1. 错综复杂的业务逻辑
    业务规则自身复杂或者与其他处理交织在一起。
    解决方法:设计模式(责任链、装饰者、策略、状态)

3. 测试遗留代码

在重构遗留代码时,自动化测试能有效的保证重构不会在无意中影响软件的行为。

  1. 增加单元测试
  2. 单元测试不是万能的(方法-》单元测试,模块-》功能测试,组件-》集成测试,系统-》系统测试)
  3. 别过度追求测试覆盖率(70左右)
  4. 自动化所有测试

流程的顺序:结对编程、代码评审、单元测试、功能测试、集成测试、系统测试、UI测试、性能测试、负载测试、冒烟测试、模糊测试等候,还要经过用户测试(渐进式发布新版本、收集真实用户数据、执行新版本的隐藏发布)

5. 重新架构

1. 什么是重搭架构

重新架构比方法和类的级别更高。重构是将一些类移动到单独的包中,重新架构是要从主代码库移动到从代码库。

将应用程序拆分为组件或成熟服务的主要目标:

  1. 通过模块化内建质量
  2. 良好的设计保障可维护性
  3. 通过独立达到自治

2. 将单体应用分级为模块

  1. 起点
    画出模块化之前的程序图例
  2. 背景
    重构的原因
  3. 项目目标
    确定在项目完成时期望的状态。
  • 引入显示接口。模块之间只能通过接口交互,方便mock实现。
  • 将源代码拆分为模块。使容易使用,依赖关系明确。
  • 改善依赖管理。
  • 清理并简化构建脚本。分模块后,构建更复杂了。
    慎重的目标:
  • 系统架构的变化
  • 功能更改
  1. 定义模块和接口
    画出期望的模块
  2. 构建脚本和依赖管理
    maven
  3. 分拆模块
    模块框架就位后,定义接口并将源代码移动到模块中就相对容易。从最简单的模块开始。对于复杂的模块,使用依赖分析工具来定位这循环依赖,要经过多次耐心重构才可能达到目标。
  4. 结论

6. 大规模重写

1. 决定项目范围

  1. 项目目标是什么
  • 黑盒式重写:保持功能与现在一直,但内部从头开始重新实现。可能是移植到新的技术栈或者让软件更容易维护。
  • 温习式重写:使新软件的功能与旧的不同。
  • 补偿式重写:开发一些新功能作为重写项目的一部分。
  1. 记录项目范围
    项目范围文档中包括以下信息:
  • 新功能(你要添加任何新功能吗?)
  • 现有功能(你计划删除现有的软件功能吗?)
  • 及时性与功能完整性(某个日期发布一个迭代)
  • 分阶段发布(总结每个阶段的内容)

使用迭代方法,即做一些小的发布,并在每次发布中添加更多工鞥呢。

2. 从过去学习

3. 如何处理数据库

将新软件连接到现有数据库;创建爱你一个新数据库,然后做数据迁移

  1. 共享现有数据库
    优点:简单(无数据迁移),无需更新其它应用程序和脚本
    缺点:无法选择数据存储技术,无法重新架构,无法重构数据库表,有损坏数据的风险
  2. 创建新数据库
    实时数据同步、批量同步

7. 停止编写遗留代码

1. 源代码并不是项目的全部

除了代码

  • 技术因素:开发工具、自动化配置、持续集成,简化发布和部署流程
  • 组织因素:文档、团队内部及团队之间的交流、软件质量文化、其它部门的压力

2. 信息不能是自由的

  1. 文档
    编写、维护文档代价大。
    有价值的文档:
  • 信息丰富(做什么、如何做、为什么这么做)
  • 易编写
  • 易发现
  • 易阅读
  • 可信赖
    应该定期评审自己的文档,并删除所有过时的文档。
  1. 促进沟通
  • 代码评审
  • 结对编程
  • 技术访谈:展示他们的技术或者橙果,每个人都可以了解其他人在做什么
  • 向其他团队展示你的项目:向其他团队介绍产品的技术概述。
  • 黑客日:多个团队合作,使用新技术,构建很炫酷的东西。

3. 工作是做不完的

小洞不补,大洞吃苦。越早修复技术债务,就越容易修复。

  1. 代码评审
  • 做笔记
  • 开发人员引导,花几分钟介绍代码库,然后询问周围人的意见
  • 评审要控制时间,可以多次评审
  • 写出评审结果清单,给出具体行动或者想法,共享文档,几周后检查进展情况。
  1. 修复一扇窗户
    如果一个破窗不修复,会有更多的破窗,混乱就会迅速增多。

4. 自动化一切

自动化测试、自动化构建、部署以及其他Jekins任务、自动化配置。
自动化的原因:减少错误、减少重复劳动。

5. 小型为佳

模块化是首要任务,保持代码库轻盈灵活。如果船有腐烂模板,将其替换,就能使船保留几个世纪。

7. 持续交付的软件系统架构

程序员的呐喊:

  • 所有的团队都要以服务接口的方式,提供数据和各种功能
  • 团队之间必须通过接口通信
  • 不允许任何其他形式的操作:不允许直接读取其他团队的数据,不允许任何形式的后门。只能通过网络调用服务
  • 具体实现技术不规定,Http、Pub\Sub等。
  • 所有的服务接口,必须从一开始就一可以公开为设计导向

1. 大系统小做原则

  1. 持续交付架构要求
  • 为测试而设计。能快速进入测试环节、方便测试
  • 为部署而设计。降低部署花费的时间
  • 为监控而设计。能对其监控,无需等客户反馈问题。
  • 为扩展而设计。支持团队成员规模的扩展;支持系统自身的扩展。
  • 为失效而设计。一旦部署或发布失败,如何优雅且快速的处理。
  1. 系统拆分原则
  • 作为系统的一部分,每个组件或服务有清晰的业务职责,可以被独立的修改,甚至被替代
  • 高内聚、低耦合,每个组件或服务只知道尽可能少的信息,完成相对独立的单一功能。
  • 整个系统易于构建与测试。拆分后,这些组件仍然需要组合在一起,为用户提供服务。避免无法测试的情况。
  • 使团队成员之间沟通协作更方便。

2. 常见架构模式

插件、单体、微服务

3. 架构改造实施模式

拆迁者、绞杀者、修缮者模式、数据库拆分。
拆迁者模式:根据当前业务需求,对软件架构重新设计,并组织单独团队,重新开发一个全新的版本,一次性完全替代原有的遗留系统。
好处:与旧系统无关。
缺点:

  • 功能遗漏
  • 无时间应对市场需求变化
  • 人力资源消耗大。一部分人维护旧版本,一部分人进行重构
  • 闭门造车:新版本上线后,无法满足业务需求。

绞杀者模式:保留原有遗留系统不变,通过不断构建新的服务,逐步使遗留系统失效,最终替代它。
好处:不会遗漏原有功能;可以稳定提供价值,频繁交付版本,方便监控改造进展。
坏处:架构改造时间过大;产生一定的迭代成本。

修缮者模式:将遗留系统的部分功能与其余部分隔离,以新的架构进行单独改善。改造只发生在同一个系统内部,而非遗留系统外部。
优点:系统外部无感知;不会遗漏原有需求;可以随时停下改造工作,响应高优先级的业务需求;避免“闭门造车”现象。
缺点:架构改造时间过长;会有额外的架构改造迭代成本。

数据库改造
数据库是单体应用的最大耦合点。参考如下步骤:

  1. 详细了解数据库结构
  2. 先拆分数据库,做数据库迁移
  3. 将拆分出的程序模块和数据库组合在一起,形成微服务。

8.建立成功的开发文化

1. 定义“成功”

2. 程序设计文化

3. 公司文化

4. 成功的程序设计文化的特征

你可能感兴趣的:(重构基本理论)