[Golang]一个工单系统的重构过程-FP vs OOP

背景

组内的数据管理平台承担着公司在线特征数据的管理工作。开发通过提交工单接入我们的数据系统。工单模型在设计之初只考虑到了一种类型的工单(新特征的申请),对于工单生命周期的每个节点分别用一个接口去实现。随着业务迭代,还有一些操作也需要通过走工单让管理员审批执行。此时最初的工单模型不能满足需求,此时为了让系统先用起来,我们的做法是写单独的接口去实现...这样虽然能用,但是导致后端代码里多出来了很多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地址
欢迎关注我的公众号:薯条的自我修养

推荐阅读更多精彩内容