[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地址
歡迎關注我的公眾號:薯條的自我修養

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容