记录:每日学习一点点, 每日进步一点点。
问题分析
近期在项目的一个功能的处理,因着急上线就着急完成功能开发后测试没有问题后就直接上线,导致存在一系列的坏代码的味道:
- 代码不够简洁,命名存在不够通俗易懂现象
- biz层臃肿,混杂本应由其他层处理的逻辑代码,其中存在一个方法已经壮大到有300多行的代码量
- 可读性差
- 扩展性差,功能融合一个方法中增加了单测的难度
现状的项目结构分层
主要使用了 kratos 框架搭建的一个项目,该功能主要是异步处理数据,其中主要流程是读取数据,判断是否可推送,发布提醒,推送数据。
ser/
├── biz/ # 业务逻辑 (包含用例编排)
│ └── user.go
├── service/ # 服务层
│ ├── dataServ.go # 权限校验
├── data/ # 数据访问
│ ├── userRepository.go
│ └── groupRepository.go
部分代码
#dataServ.go
ctx := context.context()
user.DataHandle(ctx)
#user.go
type UserUsecase struct {
UserRepository
GroupRepository
}
func (u *UserUsecase) DataHandle(ctx contxt.context){
// 获取待操作数据
dataKey:= user.repo.getDealData()
data := user.repo.getData(dataKey)
// 获取数据配置,判断是否可执行
conf := user.repo.getDataConf()
if conf.isUsed && conf.canSend(){
}
if datas.state==1 {
// 数据状态可执行
}
// 初始化处理器
handle := initDataHandler
// 推送开始处理数据
// 处理数据
// 推送处理结束
// 更新处理结束状态
}
优化方案
垂直拆分
先根据业务逻辑将功能进行垂直拆分,拆分成独立的方法,保证每个方法职责单一,只负责一件事。根据以下特征考虑是否使用垂直拆分:
- 一个方法中包含多个可以独立的流程,比如订单的付款流程:创建订单、订单校验、订单支付等
- 部分功能的修改互不影响且修改频率差别大
- 当无法明确边界时可以先考虑使用垂直拆分法
以下是拆分后的代码结构:
type UserUsecase struct {
UserRepository
GroupRepository
}
func (u *UserUsecase) DataHandle(){
IsCanSend(u.UserRepository,u.GroupRepository)
data :=GetData(u.GroupRepostory)
SendData(data)
SendNotice()
UpdateState(u.GroupRepository)
}
接下来介绍学习到另一种拆分方式:水平拆分。
水平拆分
当我们的代码中存在很多和业务没有关系的代码时可以考虑将这部分拆分出来,比如一些基础的设施的调用。在数据处理的业务中存在初始化处理器和推送通知消息,类似这些和业务逻辑无关的可以进行水平拆分。
type UserUsecase struct {
UserRepository
GroupRepository
noticer NoticeSender
auditLog AuditLogger
}
func (u *UserUsecase) DataHandle(){
IsCanSend(u.UserRepository,u.GroupRepository)
data :=GetData(u.GroupRepostory)
u.noticer.sendAlarm("开始推送数据")
SendData(data)
u.noticer.sendAlarm("推送数据技术")
UpdateState(u.GroupRepository)
u.auditLog.Log("user_handle", u.ID)
}
发通知和审计日志是基础设施,这样让功能清晰明了,且在扩展性上后续如果切换noticer 的成本也会很低,无需改动太多的功能。
什么样的代码才算是整洁的代码
在优化的过程中也查看了一些资料,整理了让自己代码整洁时的一些设计:
- 可读性强,当看到代码就能知道该代码的功能
- 命名:简洁且有意义的名称。 函数名应该用动词,如“GetUser()”。这样的名称,
- 清晰且简单的变量:u->user,h->handler
- 避免不必要的冗余命名:如使用“User”而不是“UserInfo”
- 函数名应该用动词,如GetUser()、ValidateOrder()
- 包名要简短的单数命名,避免使用common、util。要具体化,比如“stringutil”。
- 布尔值命名:如isOpen,hasPermission
- 函数
- 长度20-50之间
- 参数数量<= 3个,超过使用结构体封装
- 满足设计模式:单一职责、开闭原则、里氏替换原则 等原则
- 类,整洁的类应该满足:单一职责、开闭原则、高内聚