美文网首页深入浅出golang
[Golang]一个工单系统的重构过程-FP vs OOP

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

作者: 一根薯条 | 来源:发表于2020-02-23 16:31 被阅读0次

    背景

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

    相关文章

      网友评论

        本文标题:[Golang]一个工单系统的重构过程-FP vs OOP

        本文链接:https://www.haomeiwen.com/subject/sbejqhtx.html