我正在寻找一个合适的项目来演示如何重构。事实上,我已经找到了一些,但是它们的复杂度不太适合在线教学,所以我正在努力简化出一个可用的版本。
如果你已经迫不急待的话,可以尝试对以下的项目进行重构:
它们是我在 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());
}
...
嗯,这是一个典型的贫血模型设计,这些业务逻辑都可以内聚到领域模型中。
所以,我们有了初步的结论,可以尝试的内容:
- 典型三层架构。可以业务维度重新做分层架构
- 贫血模型。可以重构到充血模型
工具评估
现在,是时候拿出我的 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 次,我决定打开这个文件看看:
- 有一个 150 行左右的方法
- 总行数 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
这个相当于是没有测试吧。
重构策略
- 进行 DDD 建模
- 搭建 E2E 测试
- 引入 Flyway 做数据库迁移
- 分层架构重构
- 重构到充血模型
- ……
下一节:数据库重构,是对数据库 schema 的一个简单变更,在保持其行为语义和信息语义的同时,改进了它的设计。 —— 《数据库重构》