重构示例

我正在寻找一个合适的项目来演示如何重构。事实上,我已经找到了一些,但是它们的复杂度不太适合在线教学,所以我正在努力简化出一个可用的版本。

如果你已经迫不急待的话,可以尝试对以下的项目进行重构

它们是我在 GitHub 上找到的 Star 数相当多的开源 Java 项目,所以让我们来进行评估吧。

这两个项目的作者在项目中展示了极高的专业性和原则性。无论怎么说,这都是“好代码”,我也并非出于恶意的目的。如果你看了我在 GitHub 上的项目,你也会对我有诸多吐槽。我所尝试去做的是,以专业眼光来检视问题,不多也不少。作为一个追求匠艺的手工艺人,我们应该欢迎别人对我们这么做。只有批评才能让我们学到更多的东西。在进一步的重构之前,我们要再一次感谢两位作者将代码免费给社区的勇气和信心。他们做得相当的好。

相比之下,我大抵就只会复制和粘贴内容。

开始之前,你大可以从 GitHub 上复制这两个项目的其中一个,然后这里的例子以 mall 为例。你可以将 zheng 作为你的练手项目。

评估

这两个项目都有丰富的文档,足够让你搭建好它们的环境。事实上,我觉得你可能不需要这样的操作,你需要打开你的 Intellij IDEA,然后构建一下,就可以阅读代码了。

C4 模型展开

首先,让我们以 C4 模型来展开这个项目的代码。

展开系统目录结构 。从目录结构上来看,mall 系统的组织相当的不错,按业务和通用模块进行了拆分。

├── mall-common -- 工具类及通用代码
├── mall-mbg -- MyBatisGenerator生成的数据库操作代码
├── mall-security -- SpringSecurity封装公用模块
├── mall-admin -- 后台商城管理系统接口
├── mall-search -- 基于Elasticsearch的商品搜索系统
├── mall-portal -- 前台商城系统接口
└── mall-demo -- 框架搭建时的测试代码

展开其中的一个微服务 。这里以 mall 为例,这是典型的 controller-service-dao 架构:

mall-admin
├── bo
├── component
├── config
├── controller
├── dao
├── dto
├── service
└── validator

而其中的 home 包,又以 展开包架构 。接着,让我们看看 controller 包下的目录结构,这是按技术划分服务的架构模式:

controller
...
├── SmsFlashPromotionController.java
├── SmsFlashPromotionProductRelationController.java
├── SmsFlashPromotionSessionController.java
├── SmsHomeAdvertiseController.java
├── SmsHomeBrandController.java
├── SmsHomeNewProductController.java
├── SmsHomeRecommendProductController.java
├── SmsHomeRecommendSubjectController.java
├── UmsAdminController.java
├── UmsMemberLevelController.java
├── UmsPermissionController.java
└── UmsRoleController.java

展开类 。接着,让我们查看一下 OmsOrderReturnApplyController.java 文件,看看最后的接口:

@ApiOperation("修改申请状态")
@RequestMapping(value = "/update/status/{id}", method = RequestMethod.POST)
@ResponseBody
public CommonResult updateStatus(@PathVariable Long id, @RequestBody OmsUpdateStatusParam statusParam) {
    int count = returnApplyService.updateStatus(id, statusParam);
    if (count > 0) {
        return CommonResult.success(count);
    }
    return CommonResult.failed();
}

用 OmsUpdateStatusParam 封装了请求参数,这个实践相当的不错。接着,我们进入 updateStatus 方法看看:

@Override
@Override
public int updateStatus(Long id, OmsUpdateStatusParam statusParam) {
    Integer status = statusParam.getStatus();
    OmsOrderReturnApply returnApply = new OmsOrderReturnApply();
    if(status.equals(1)){
        //确认退货
        returnApply.setId(id);
        returnApply.setStatus(1);
        returnApply.setReturnAmount(statusParam.getReturnAmount());
        returnApply.setCompanyAddressId(statusParam.getCompanyAddressId());
        returnApply.setHandleTime(new Date());
        returnApply.setHandleMan(statusParam.getHandleMan());
        returnApply.setHandleNote(statusParam.getHandleNote());
    }else if(status.equals(2)){
        //完成退货
        returnApply.setId(id);
        returnApply.setStatus(2);
        returnApply.setReceiveTime(new Date());
        returnApply.setReceiveMan(statusParam.getReceiveMan());
        returnApply.setReceiveNote(statusParam.getReceiveNote());
    }
		...

嗯,这是一个典型的贫血模型设计,这些业务逻辑都可以内聚到领域模型中。

所以,我们有了初步的结论,可以尝试的内容:

  1. 典型三层架构。可以业务维度重新做分层架构
  2. 贫血模型。可以重构到充血模型

工具评估

现在,是时候拿出我的 Coca:https://github.com/phodal/coca

代码统计

执行一下 coca cloc 可以看看项目的行数统计:

───────────────────────────────────────────────────────────────────────────────
Language                 Files     Lines   Blanks  Comments     Code Complexity
───────────────────────────────────────────────────────────────────────────────
Java                       471     80837    16265      2276    62296       1405
XML                        112     21710       61       119    21530          0
YAML                        12       430       36         8      386          0
Markdown                    10      1211      271         0      940          0
JSON                         8   1345664        0         0  1345664          0
gitignore                    6       119       15        17       87          0
Shell                        3        46        0         3       43          0
SVG                          2      6132        0       988     5144          0
License                      1       201       32         0      169          0
Properties File              1         4        0         0        4          0
SQL                          1      2192      143       440     1609          0
───────────────────────────────────────────────────────────────────────────────
Total                      627   1458546    16823      3851  1437872       1405
───────────────────────────────────────────────────────────────────────────────
Estimated Cost to Develop $55,872,945
Estimated Schedule Effort 70.766444 months
Estimated People Required 93.525243
───────────────────────────────────────────────────────────────────────────────

基本情况评估

先执行一下 coca analysis,然后 coca evaluate,得到一个基本的情况

TYPE COUNT LEVEL TOTAL RATE
Nullable / Return Null 21 Method 13757 0.15%
Utils 2 Class 604 0.33%
Static Method 7 Method 13757 0.01%
Average Method Num. 13757 Method/Class 604 22.776490
Method Num. Std Dev / 标准差 13757 Class - 52.137890
Average Method Length 46177 Without Getter/Setter 11218 4.116331
Method Length Std Dev / 标准差 13757 Method - 2.928149

从数据上看,静态方法只有 7 个,返回 null 的情况有 21 个,保持得不错。项目的平均方法长度也还行,就是平均方法有点多。

代码坏味道评估

接着试试 coca bs -s=type 来查看代码中的常见坏味道,限于篇幅的原因这里就不复杂了,说主要问题:大的类一共有 72 个,其中 OmsOrderExample.java 类,在没有 set/get 的情况下有 576 个方法。查看了一下引用情况,好像是来看数据库查询用的……:

public Criteria andProductSnLike(String value) {
    addCriterion("product_sn like", value, "productSn");
    return (Criteria) this;
}

这要重构的话是个体力活,详细见 coca_reporter/bs.json

架构评估

执行了一下 coca arch,由于是扁平的三层架构,没有太多的问题。

API 评估

执行 coca api -c -r com.macro.mall. 获得基本的 API 列表情况:

SIZE METHOD URI CALLER
5 GET /prefrenceArea/listAll controller.CmsPrefrenceAreaController.listAll
5 GET /subject/listAll controller.CmsSubjectController.listAll
18 GET /subject/list controller.CmsSubjectController.getList
28 POST /minio/upload controller.MinioController.upload
13 POST /minio/delete controller.MinioController.delete
5 GET /companyAddress/list controller.OmsCompanyAddressController.list
18 GET /order/list controller.OmsOrderController.list
11 POST /order/update/delivery controller.OmsOrderController.delivery
11 POST /order/update/close controller.OmsOrderController.close
11 POST /order/delete controller.OmsOrderController.delete

这里的 size 指的是调用的方法里,这里的 MinioController 的 upload 方法里一共调用了 28 个方法,打开一看:

@ApiOperation("文件上传")
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public CommonResult upload(@RequestParam("file") MultipartFile file) {
    try {
        //创建一个MinIO的Java客户端
        MinioClient minioClient = new MinioClient(ENDPOINT, ACCESS_KEY, SECRET_KEY);
        boolean isExist = minioClient.bucketExists(BUCKET_NAME);
        ...
        minioClient.putObject(BUCKET_NAME, objectName, file.getInputStream(), file.getContentType());
        LOGGER.info("文件上传成功!");
        MinioUploadDto minioUploadDto = new MinioUploadDto();
        minioUploadDto.setName(filename);
        minioUploadDto.setUrl(ENDPOINT + "/" + BUCKET_NAME + "/" + objectName);
        return CommonResult.success(minioUploadDto);
    } catch (Exception e) {
        LOGGER.info("上传发生错误: {}!", e.getMessage());
    }
    return CommonResult.failed();
}

嗯,它可能需要一个 service,而考虑到 Minio 是一个云存储服务器,还需要通过接口来封装这些细节。

API 架构图

在生成 API 结果之后,可以打开 coca_reporter/arch.svg 查看项目的架构图。不过,由于项目的 API 较多,便需要一个个分析,所以你可以通过 coca api -c -r com.macro.mall. -a /order 查看 /order 的所有接口情况:

SIZE METHOD URI CALLER
17 GET /order/list controller.OmsOrderController.list
11 POST /order/update/delivery controller.OmsOrderController.delivery
11 POST /order/update/close controller.OmsOrderController.close
11 POST /order/delete controller.OmsOrderController.delete
5 GET /order/{id} controller.OmsOrderController.detail
11 POST /order/update/receiverInfo controller.OmsOrderController.updateReceiverInfo
11 POST /order/update/moneyInfo controller.OmsOrderController.updateReceiverInfo
11 POST /order/update/note controller.OmsOrderController.updateNote
5 GET /orderSetting/{id} controller.OmsOrderSettingController.getItem
11 POST /orderSetting/update/{id} controller.OmsOrderSettingController.update
5 POST /order/generateConfirmOrder portal.controller.OmsPortalOrderController.generateConfirmOrder
2 POST /order/generateOrder portal.controller.OmsPortalOrderController.generateOrder
2 POST /order/paySuccess portal.controller.OmsPortalOrderController.paySuccess
2 POST /order/cancelTimeOutOrder portal.controller.OmsPortalOrderController.cancelTimeOutOrder
5 POST /order/cancelOrder portal.controller.OmsPortalOrderController.cancelOrder

高引用 + 高修改分析

执行 coca count 可以查看高引用的方法:

REFS COUNT METHOD
8055 com.macro.mall.model.GeneratedCriteria.addCriterion
199 com.macro.mall.common.api.CommonResult.success
125 com.macro.mall.common.api.CommonResult.failed
30 com.macro.mall.model.GeneratedCriteria.addCriterionForJDBCDate
23 com.macro.mall.common.api.CommonPage.restPage
20 com.macro.mall.model.GeneratedCriteria.addCriterionForJDBCTime
17 com.macro.mall.portal.service.UmsMemberService.getCurrentMember
17 com.macro.mall.model.UmsMember.getId
16 com.macro.mall.service.impl.PmsProductServiceImpl.relateAndInsertList
7 com.macro.mall.portal.domain.OrderParam.getUseIntegration
7 com.macro.mall.portal.domain.OrderParam.getCouponId

看上去,主要问题还在数据库查询语句拼接那一部分。执行 coca git -t 可以查看经常修改的文件:

ENTITYNAME REVSCOUNT AUTHORCOUNT
README.md 121 2
document/pdm/mall.pdm 29 2
document/pdm/mall.pdb 26 2
mall-admin/pom.xml 23 2
mall-portal/pom.xml 18 2
document/sql/mall.sql 16 2
mall-portal/src/main/java/com/macro/mall/portal/service/impl/OmsPortalOrderServiceImpl.java 14 2
mall-admin/src/main/java/com/macro/mall/controller/PmsBrandController.java 14 2
document/reference/deploy-windows.md 14 2
mall-search/pom.xml 14 2
document/docker/docker-deploy.md 13 2

经常修改的地方是 pom 文件和文档,看上去没啥问题。考虑到 OmsPortalOrderServiceImpl.java 文件修改了 16 次,我决定打开这个文件看看:

  1. 有一个 150 行左右的方法
  2. 总行数 643 行

明显这是一个订单相关的上帝类,关联的 OmsOrder 模型有 40 ~ 50 左右的字段。毫无疑问,这里就是代码中经常出现问题的地方。

测试

执行了 coca tbs,一共找到了这几个文件

Start parse java call: PmsDaoTests.java
Start parse java call: MallDemoApplicationTests.java
Start parse java call: MallPortalApplicationTests.java
Start parse java call: PortalProductDaoTests.java
Start parse java call: MallSearchApplicationTests.java

这个相当于是没有测试吧。

重构策略

  1. 进行 DDD 建模
  2. 搭建 E2E 测试
  3. 引入 Flyway 做数据库迁移
  4. 分层架构重构
  5. 重构到充血模型
  6. ……
下一节:数据库重构,是对数据库 schema 的一个简单变更,在保持其行为语义和信息语义的同时,改进了它的设计。 —— 《数据库重构》