跳到主要内容

系统重构

Create by fall on 30 Jan 2022 Recently revised in 21 Nov 2024

系统重构

软件开发成本 = 开发成本 + 维护成本;软件维护成本 = 软件理解成本 + 修改成本 + 测试成本 + 部署成本。 ——Kent Beck

更新、迁移、重构、重写、重搭。

重写,规范代码,更新技术栈。

技术债管理由于重构。

为什么重构

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整期结构。

从这些定义来看,就是之前的系统特性(bug)也要保持该特性,然后让代码可读性变强。

重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。——《重构:改善既有代码的设计》

你觉得代码好看还是难看,那只是因为你熟悉或陌生。

重构就是为了钱,如果不能带来潜在的价值,就不要谈重构。

重构的动机

  • 改善既有代码设计
  • 帮助找到软件中的 bug(注意,不是修复,而是把出问题的代码找到)
  • 提升开发效率
  • 提高编程的趣味性——或许。

重构不一定带来

重构的投入与回报并非呈现线性关系。——《领域驱动设计:软件核心复杂性对应之道》

毕竟味道越重的代码,相同的重构结果,也会提升更高。

人们总倾向于认为能够带来:更少的代码;更稳定的系统,等等。。。

但是一般来讲,重构是这样的:

  • 我们拆分一个上帝类的时候,我们可能拆出了三四个类,因为那些 Import 、package、 type 的 declaration, 反而带来了更多的代码。
  • 当我们发现重复代码,并且它们是可以被抽象的,那么我们就可以消去一般的代码。
  • 如果我们在重构的过程中,发现 bug,那么系统必然会更加稳定。
  • 如果因为我们重构,导致憋人合并代码时发生冲突,反而会带来 bug。
  • ......

尽管我们可能不会受益于所有重构的好处,但依然可以带来

  • 编程的技艺。稳定的业务代码,便容易选择合理的识别设计模式。
  • 重构的手法
  • 持续重构的意识
  • 模式和原则。相似问题的处理使用相似的手法。
  • 抽象能力

大规模重构

在接手别人或者别的新系统的代码,此时进行大规模重构。我们往往不被允许重构,并且如果业务人员说要重构的时候,系统可能不是重构那么简单,也许需要重写。

系统重构模式与原则

重构的基本原则

  • 小步前进。走一小步,提交一次代码,方便回滚。
  • 随时可用。如果不能保证随时可用,那就说不上是重构了。(我理解的为,你重构一个功能后,能够继续使用)
  • 融入日常。

PDCA(Plan、Do、Check、Act)做出计划,实施计划,检查效果,纳入标准。不成功的作为下一轮去处理。作者在最前面加了个 E,evaluate(评估,识别需要重构的地方)

四级重构

  • 架构重构。在不改变业务逻辑的情况下,根据单依职责和依赖倒置原则的思想:对系统进行模块拆分和合并,以明确职责,降低耦合度;对包进行重规划,划分包之间的便捷,减少代码的耦合。(用以明确不同业务包的作用)
  • 模型重构。在包含测试的情况下,通过识别和发现模型的行为,将行为聚合到模型中:根基方法名,参数,返回判定内聚到模型中;从流程梳理是否符合业务场景。
  • 模式重构。对于特定代码坏味道产生的问题。通过结合架构模式、设计模式来提升可读性。如使用工厂模式统一管理对象的创建;使用策略模式降低复杂度。
  • 代码重构。对于一些小的代码坏味道,可以通过 IDE 重构来快速改善既有代码,而不会影响到业务功能。复杂条件语句的提取,使用参数对象冲狗狗参数过多。

小步前进:每隔几十行代码(同一个逻辑),随时提交,以及同步代码,方便回滚

人员评估

  • 确保有足够的能力
  • 确保对于重构有共同的看法
  • 确保彼此能配合工作

并且具备以下技能:

  • 理解面向对象设计
  • 了解设计模式
  • 了解测试的重要性
  • 了解为什么要重构
  • 追求代码质量

重构范围

对于庞大的系统来说,每一部分的价值并非相等。

对于系统的核心,为它分配最好的开发人员,对于支撑部分来说,分配少量核心开发人员,确保工作能够按时完成。

按照 DDD 的思想来看,就是核心域,支撑域,通用域的区别。

重构评估与度量

技术债务

对于技术债,利息表现在系统的不稳定性,以及由于临时性手段和缺乏合适的设计、文档工作和测试带来的不断攀升的维护成本。—— 《软件架构师应该知道的 97 件事》

不可见(可维护性,可演进性两个方面):

  • 架构:
    • 架构坏味道
    • 反模式
    • 复杂的结构
  • 代码:
    • 内部质量低下
    • 代码复杂
    • 代码花味道
    • 违反编程风格
  • 其他开发制品:
    • 测试和文档的问题

可见问题:

  • 可演进性:
    • 新功能(新增功能)
  • 可维护性:
    • bug(缺陷)
    • 外部质量低下

借助 Tequila 架构可视化工具,可以得到项目的调用关系图,某种层面上反映出系统的架构

  • 项目的结构划分是否合理
  • 项目中的代码是否存在循环依赖的情况

如何找到技术债务的位置?

收集代码坏味道,coca bs

Todo 一般 todo表示应该去做的事,但是没有立刻去做,这些事可能就是项目中的技术债务。

重构准入条件

  • 工具准备
  • 重构看板
  • 版本控制
  • 自动化构建工具
  • 持续集成环境
  • 记录
  • 探索性重构:可行方案
  • 准备知识:坏味道模式

重构:改善既有代码的设计中提出的23种代码坏味道

  • 代码臃肿。过长函数,过大的类,基本类型偏执,过长参数列,数据泥团
  • 滥用面向对象。Switch 声明,临时字段,被拒绝的遗赠,异曲同工的类
  • 变革的障碍。发散式变化,散弹式修改,平行继承体系。
  • 非必要的。冗余类,纯稚的数据类型,重复代码,注释,没有使用的未来特性。
  • 耦合。不完美的类库,依恋情节,消息链,中间人

C4模型:System、Container、Component、Code 四个层次,由顶部到底部介绍系统的架构。

探索模式

从分层架构到具体代码

  • 代码库关系
  • 代码库内模块化结构
  • 模块化包的结构
  • 包内代码结构

从外部适配器到内部适配器

  • 是否包含单元测试
  • 是否包含集成测试
  • 测试覆盖率情况
  • 测试编写情况

最复杂的情形和最简单的情形

  • 从最简单的场景出发,对常规流程,包间关系有一定的了解
  • 从复杂的场景收尾,看最复杂的场景下会有什么问题
  • 根据需要寻找一个适合的场景。

寻找高引用和修改

识别高修改的文件,用来查看文件的修改次数,如果是上帝类经常进行修改,说明文件经常出现问题。重构着重考虑。

系统架构重构

防护网

重构不影响 API 的使用方,我们需要设计合适的防护策略。

检视测试(表明测试中的问题,测试代码坏味道)

  • 空的测试。测试是生成的,但是没有内容。
  • 忽略的测试。测试被 ignore
  • 没有断言的测试。为了提高测试覆盖率而出现的测试。
  • 多余的 PrintIn。调试时留下的信息。
  • 多重断言,每个测试函数只应该测试一个概念。

架构将大问题分解为容易处理的小问题。——《架构师修炼之道 》

重量级重构

重量级重构,意味着重构时需要有:

  • 积极专注的团队参与
  • 复杂的场景
  • 领域专家的协助
  • 迭代式的模型设计

为此,之前需要进行

  • 事件风暴
  • 识别上下文,梳理上下文关系
  • 划分问题子域
  • 识别弹性边界
  • 领域建模
  • 分层架构

实现时,需要:

  • 重搭架构
  • 编写集成测试
  • 搬移代码

轻量级重构

  • 结合工具识别出所有的接口
  • 通过接口识别出领域名词(聚合、实体)
  • 分析接口设计是否合理
  • 通过领域名词划分上下文边界
  • 重新定义领域名词
  • 结合领域名词划分新的 API
  • 划分领域和分层架构
  • 重新划分分层架构
  • 重构 service 代码,剥离领域逻辑
  • 重构代码到领域模型

限界上下文要素

  • 实体(entity)。只要一个对象在生命周期中能够保持连续性,并且独立于它的属性(即:使这些属性对系统用户非常重要),那它就是一个实体。具有唯一标识和生命周期。
  • 值对象(value object)。当你只关心某个对象的属性时,该对象便可作为一个值对象。它是实体的附加业务概念,用来描述实体所包含的业务信息。
  • 领域服务(domain service)。封装了没有在模型中自检建模为值对象或实体的领域逻辑概念。职责是使用实现和值对象编排业务逻辑。
  • 领域事件(domain event)。他用于表明问题域中发生了一些业务人员关心的事情。在明明领域事件时,我们往往选择动词的过去分词,已明确表达事件的属性,(XXX 已经 YYY)
  • 资源库(repository)。公开聚合根在内存中的集合的接口,提供聚合根的检索和持久化需要。
  • 工厂(factory)。即在实体或者值对象创建复杂时,可以委托给工厂(模式)进行创建。
  • 聚合(aggregate)。一种边界内的领域对象集群,可以将其视为一个单元。可以封装一个到多个实体与值对象,用来维护该边界范围之内的业务完整性。

微服务重构

一旦涉及到对微服务进行重构,有很大的可能性要对系统进行 API 的重构设计。我们可能会影响到API的使用方,如果使用方是第三方团队的时候,我们就要考虑一下兼容使用方案。这种兼容方案会影响我们的重构。

微服务是一个生态系统,它需要大量的基础设施进行配合,如部署管道、服务注册与发现,日志和监控,负载均衡。

该文章笔者对微服务重构也不精,敬请期待我的下一次更新!

服务架构重构

(容器架构重构)

Robert C. Martin 总结了六边形架构(即端口与适配器架构)、DCI (Data-Context-Interactions,数据-场景-交互)架构、BCI(Boundary Control Entity,Boundary Control Entity)架构等多种架构,归纳出了这些架构的基本特点:

  • 框架无关性。不依赖于框架中的某一个函数,框架只是一个工具,系统不能,不去适应框架。
  • 可被测试。业务逻辑脱离于 UI、数据库等外部元素进行测试。
  • UI 无关性。不需要修改系统的其它部分,就可以变更 UI,诸如由 Web 界面替换成 CLI。
  • 数据库无关性。业务逻辑与数据库之间需要进行解耦,我们可以随意切换LocalStorage、IndexedDB、Web SQL
  • 外部机构(agency)无关性。系统的业务逻辑,不需要知道其它外部接口,诸如安全、调度、代理。

这段不怎么会

实体(Entities),实体用于封装企业范围的业务规则。实体可以是拥有方法的对象,也可以是数据结构和函数的集合。如果没有企业,只是单个应用,那么实体就是应用里的业务对象。这些对象封装了最通用和高层的业务规则,极少会受到外部变化的影响。任何操作层面的改动都不会影响到这一层。

用例(Use Cases),用例是特定于应用的业务逻辑,一般用来完成用户的某个操作。用例协调数据流向或者流出实体层,并且在此过程中通过执行实体的业务规则来达成用例的目标。用例层的改动不会影响到内部的实体层,同时也不会受外层的改动影响,比如数据库、UI 和框架的变动。只有而且应当应用的操作发生变化的时候,用例层的代码才随之修改。

接口适配器(Interface Adapters)。接口适配器层的主要作用是转换数据,数据从最适合内部用例层和实体层的结构转换成适合外层(比如数据持久化框架)的结构。反之,来自于外部服务的数据也会在这层转换为内层需要的结构。

框架和驱动(Frameworks and Drivers)。最外层由各种框架和工具组成,比如 Web 框架、数据库访问工具等。通常在这层不需要写太多代码,大多是一些用来跟内层通信的胶水代码。这一层包含了所有实现细节,把实现细节锁定在这一层能够减少它们的改动对整个系统造成的伤害。

真的是 util?

实际项目中往往会向 util 内添加逻辑,正如会向 common/bean 中扔入所有的 model

阶段解释示例
空白没有名称doSomething()
凑合名称不能准确反应元素的含义preload()
沾边名称至少反映了元素某一方面的功能DomSomethingEvilToDB()
反映功能名称直接描述了元素的所有功能ParseXmlAndStoreFightToDbAndLocalCacheAndStartProcessing()
反映角色名称充分地反映了元素在架构中的角色StoreFightlightToDatabaseAndStartProcessing
反映意图名称不仅反映元素的功能,还能反映其目的。BeginTrackingFlight()
领域抽象名称超越了单个元素本身,成为一个新的抽象概念。MonitoringPanle.Add(new Flight())

数据的变化比逻辑要繁琐得多,正是这种现象让类有了存在的意义。—— 《实现模式》

对于继承的类来说,它应该遵循这么一些原则:

  • 超类名称要简单
  • 子类名称要合格

方法

在命名类和操作时要描述它们的效果和目的,而不要表露它们是通过何种方式达到目。 —— Eric Evans

描述它是用来做什么的,而不是如何实现的

  • 好的注释通过读方法的名称就能大体知道方法是用来做什么的。

消除二义性

模式重构

模式是某种场合下对某个问题的一个解决方案的一种结构化展现。 —— Jon Vlissides(GoF 成员)《设计模式沉思录》

模式相当于,解决方案的结构

我想展示的是如何用它来提升代码的可读性。也因此,我们需要干掉反模式的设计模式。

反模式

反模式:应用软件中常见的有缺陷的实现过程,主要有 4 个方面的体现:

  • 不动性:以这种方式开发的应用程序非常难以重用
  • 刚性:以这种方式开发的应用程序,任何小的修改会导致软件的大多部分必须进行相应的改动。
  • 脆弱性:当前应用程序的任何更改都会导致现有系统变得非常容易奔溃。
  • 粘滞性:由于架构层面的修改非常困难,因此修改必须由开发人员在代码或环境本身中进行。

反模式是处理重复出现问题的某些解决方案的后果。假设你遇到了一个软件设计问题,然后解决了这个问题。但是,该解决方案是否对设计产生负面影响,或影响应用程序的性能?

反模式产生的原因

  • 开发人员不了解软件开发实践
  • 开发人员没有将设计模式应用到正确的上下文中
  • 开发人员的想法会随着开发过程的推进而发生变化
  • 用例通常会随着客户的反馈而进行更改
  • 最初设计的数据结构可能会随功能或可伸缩性等方面的考虑而发生变化

单例模式

确保一个类只有一个实例,并提供一个全局访问点来访问该实例。—— 《设计模式:可复用面向对象软件的基础》

单例模式(Singleton)

单例对象存活的时间通常很长,它们通常存在于程序的整个生命周期中。一个复杂应用可能有很多个单例,会使得上述问题更加严重。

  • 对单例类的依赖被硬编码到其他类中,破坏了具体类的依赖性。
  • 单例对接口不友好。
  • 单例 getInstance 没有继承性。
  • 多线程情况下有线程安全问题。

工厂封装复杂构建

工厂是领域驱动设计中的重要组件。

工厂的目标:

  • 隐藏创建对象的复杂性
  • 减少对外暴露过多的内部结构

工厂模式是一种创建方法:

  • 工厂方法(factory method)
  • 抽象工厂(abstracty factory)
  • 建造者模式(builder)

值得注意的是:工厂并不总是需要独立的静态类。

对应的手法是:

  • 提取构建步骤到新的构建方法中
  • 将构建步骤方法转为 static 方法
  • 将这个 static 方法移到工厂类中
  • 将旧的构建函数提取成工厂方法
  • 将提取的工厂方法移到类中
  • 内联原有的工厂使用方

贫血模型

什么是贫血模型

贫血模型是指使用的领域对象中只有setter和getter方法(POJO),所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。

什么是充血模型

充血模型将大多数业务逻辑和持久化放在领域对象中,业务逻辑只是完成对业务逻辑的封装、事务和权限等的处理。比较符合面向对象。

重构实践

逻辑拆分

对项目进行拆分功能,区分职责,或者是拆分为包,作为组件去使用。

坏的拆分:根据视图去拆分(左侧菜单,右侧菜单),根据业务逻辑去拆

好的拆分:要根据实现的功能模块去拆分(无限滚动,实现富文本,实现时间的格式化),适配不同的项目。彻底拆分,尽量减少依赖

因为一部分视图同事会有多个功能,单个功能,需要用到一块完整的视图,对于分割的内容,要进行很多测试,确保可用,在一个项目里,只需要保证可用即可

这世上没有同样的成长环境,任何东西都无法绝对经得起审视,有同样的认知水平同时也没有适用于所有人的解决方案。只需代入到其中适度审视一番自己即可。能跳脱出来从第三方视角看看现在的阶段就非常可以了,具体怎么想怎么做全在你自己去不断实践中寻找那个适合自己的方案

数据库重构

数据库重构,是对数据库 schema 的一个简单变更,在保持其行为语义和信息语义的同时,改进了它的设计。 —— 《数据库重构》

换言之,你既没有增加新功能,也没有破坏原有的功能,没有添加新的数据,也没有改变原有数据的语义。值得注意的是,尽管你的领域模型发生了变化,但是这也不意味着数据库 schema 需要因此而发生改变;与此同时,一些公司的制度也会限制我们对数据库进行重构。

引入数据库迁移工具

并没有实践过大规模的数据库重构,但是和其他的后端开发者一样,在日常的开发中,我们也时不时会做一些数据库重构 —— 数据库迁移。

  • 代码版本控制
  • 数据库版本控制
  • 多数据识别

重构之后

工具的问题

架构守护测试、坏味道守护、测试驱动开发、完善基础设施

它可以作为我们的架构适应度函数,不断地见证我们架构的变化和成长 —— 以一种肉眼的方式持续演进。

解决人的问题

代码写得烂

如我们所见,我们在不同的公司里,会看到不同的人写出来的代码水平是不一样的。比如有一些是由供应商写的代码

不过,既然给这引起供应商的工资本身就不高,那么怎么能指望出他/她写出好的代码。

与此同时,由于种种原因,教他们学习的成本又特别高,甚至于你还要担心如果你教会了供应商,他/她们可能寻找一个新的工作。

养成优秀的开发习惯、人员能力提升、典型问题、练习项目、培训教程

流程的问题

我们在开发的前期缺少对于系统的设计,那么我们将需要花费大量的成本在后期修复他们。

行为驱动开发

年轻的时候(我刚毕业),我并不懂得 BDD 的好处——相当的繁琐,到了现在我又有一番不一样的体会。思来想去主要的原因是,当时我们是一个全功能的团队,不存在跨部门协作,也因此我并不觉得 BDD 能为我们带来多大的好处。

可是在 ThoughtWorks 公司之外,开发、测试和业务都是各自的部门,有着各自的利益。通过 BDD 的协作方式,可以让交付物有一个一致的观点。

参考文章

名称链接
系统重构https://migration.ink/
设计模式终章之反模式https://zhuanlan.zhihu.com/p/374019475

相关文章链接

文章名称链接
分层架构重构https://www.phodal.com/blog/refactor-mvc-architecture-to-ddd/
构建可信软件系统https://www.phodal.com/blog/build-trusted-software-system/
测试代码的坏味道https://www.phodal.com/blog/test-bad-smell/
技术债治理的四条原则https://insights.thoughtworks.cn/managing-technical-debt/

技术债治理的四条原则:

  • 核心领域优先
  • 可演进性优于可维护性
  • 明确清晰地责任定义优于松散无序的任务分配
  • 主动预防优于被动响应

相关书单

  • 《重构与模式》
  • 《设计模式:可复用面向对象软件的基础》
  • 《重构:改善既有代码的设计》
  • 《领域驱动设计:软件核心复杂性应对之道》
  • 《修改代码的艺术:构建易维护代码的 9 条最佳实践》
  • 《代码整洁之道》
  • 《架构整洁之道》
  • 《数据库重构》
  • 《遗留系统重构指南》
  • 《软件架构师应该知道的97件事》
  • 《架构师修炼之道》
  • 《实现模式》
  • 《反模式:危机中的软件,架构和项目的重构》
  • 《精益软件度量》
  • 《设计模式沉思录》
  • 《前端架构:从入门到微前端》