背景
组内的数据管理平台承担着公司在线特征数据的管理工作。开发通过提交工单接入我们的数据系统。工单模型在设计之初只考虑到了一种类型的工单(新特征的申请),对于工单生命周期的每个节点分别用一个接口去实现。随着业务迭代,还有一些操作也需要通过走工单让管理员审批执行。此时最初的工单模型不能满足需求,此时为了让系统先用起来,我们的做法是写单独的接口去实现...这样虽然能用,但是导致后端代码里多出来了很多API。趁着过年前几天业务不多,我对工单部分代码进行了重构,希望达到的效果是后续不同类型的工单复用同一套工单流程,同时减轻前后端交互的成本。
需求分析
经过抽象,对于我们的系统不同类型的工单,工单的生命周期都是一样的,工单只有这些状态:
image-20200223161953851.png
工单这几个状态要执行的操作差别是很大的,所以分别用不同接口去实现每一种工单状态,这其中代码的复用不多。工单状态和执行操作如下图:
image-20200223162320362.png
前面说到,在系统之前的代码里面不同类型的工单分别用不同的API实现,看代码可以发现,不同类型的工单在生命周期的一个节点里面做的操作是类似的。比如对于新建工单,重构代码之前操作是这样:
[图片上传中...(image-20200223162523135.png-724234-1582446375104-0)]增加工单种类之后,新建工单操作是这样:
image-20200223162523135.png
其中校验前端参数、调用工单实例、发送通知的代码都是可以复用的。只有工单操作这一块行为有所区别,工单操作简单抽象一下分为两种:
image.png
实现思路
考虑到前端同学的开发成本,这次重构复用之前的接口,在每个接口参数里面增加一项工单类型(worksheetType),根据工单类型,做不同的操作。
重构的思路有两种,一种是"函数式编程"(FP),另一种是"面向对象编程"(OOP)。这里晒出一张经典的图片,hhh...
image.png
实现对比
为了对比两种方式,分别实现了demo。
OOP如下:
package main
import (
"context"
"errors"
"fmt"
)
// -------- interface start ----------
type WorkSheet interface {
NewWorksheet(ctx context.Context, req interface{}) (interface{}, error)
ModifyWorksheet(ctx context.Context, req interface{}) (interface{}, error)
PassWorksheet(ctx context.Context, req interface{}) (interface{}, error)
RefuseWorksheet(ctx context.Context, req interface{}) (interface{}, error)
GetWorksheetInfo(ctx context.Context, req interface{}) (interface{}, error)
}
type WorksheetFactory interface {
GetWorksheetInstance(ctx context.Context, worksheetType string) (WorkSheet, error)
}
// -------- interface end -----------
// -------- worksheet instance start --------
type Caller struct{}
var CallerInstance = Caller{}
func (Caller) NewWorksheet(ctx context.Context, req interface{}) (interface{}, error) {
return fmt.Sprint(req), nil
}
// 对于不同类型的工单, 可以根据工单类型决定是否实现对应接口方法
func (Caller) ModifyWorksheet(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
}
func (Caller) PassWorksheet(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
}
func (Caller) RefuseWorksheet(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
}
func (Caller) GetWorksheetInfo(ctx context.Context, req interface{}) (interface{}, error) {
return nil, nil
}
// -------- worksheet instance end --------
// -------- WorksheetFactory instance start --------
var Factory = worksheetFactory{}
type worksheetFactory struct{}
// 用map去拿工单实例
var worksheetInsMap = map[string]WorkSheet{
"Caller": CallerInstance,
}
func (worksheetFactory) GetWorksheetInstance(ctx context.Context, worksheetType string) (WorkSheet, error) {
if _, ok := worksheetInsMap[worksheetType]; !ok {
return nil, errors.New("invalid worksheet type")
}
return worksheetInsMap[worksheetType], nil
}
// -------- WorksheetFactory instance end --------
// 这里假设main函数为NewWorksheet API
func main() {
// 项目中的变量声明可放在init函数中
var worksheetFac = Factory
// 1. 用 validator 校验参数
// 校验工作可以放在 middleware 中
// 2. 在NewWorksheet API中调用 NewWorksheet 方法
// 这里应该根据worksheetType调用对应的实例, 这里直接写死了 Caller 参数
ins, err := worksheetFac.GetWorksheetInstance(context.TODO(), "Caller")
if err != nil {
fmt.Println("error")
return
}
res, err := ins.NewWorksheet(context.TODO(), "new worksheet")
if err != nil {
fmt.Println("error")
return
}
fmt.Println(res)
// 3. 根据返回信息做通知工作
// 通知工作理论上是RPC调用,不影响工单流程,可以异步调用
}
FP如下:
package main
import (
"context"
"errors"
"fmt"
)
func CallerNewWorksheet(ctx context.Context, req interface{}) (interface{}, error) {
return fmt.Sprint(req), nil
}
func main() {
var worksheetType = "caller"
// 1. 用 validator 校验参数
// 校验工作可以放在 middleware 中
// 2. 在NewWorksheet API中调用 NewWorksheet 方法
// 这里应该根据worksheetType调用对应的实例, 这里直接写死了 Caller 参数
switch worksheetType {
case "caller":
res, err := CallerNewWorksheet(context.TODO(), "new worksheet")
if err != nil {
}
fmt.Println(res)
default:
errors.New("invalid worksheet type")
}
// 3. 根据返回信息做通知工作
// 通知工作理论上是RPC调用,不影响工单流程,可以异步调用
}
其中FP对代码的改动较小,需要重写logic层的工单逻辑,根据工单类型走一个switch操作,调用不同的工单逻辑;OOP需要增加一些接口,当有新的工单类型需要接入时,实现对应的接口方法即可,这两种方式难说谁更优秀。你可以猜猜我最后用哪种方式重构代码了 ;)
附:
项目代码github地址
欢迎关注我的公众号:薯条的自我修养
网友评论