背景
組內的數據管理平臺承擔著公司在線特征數據的管理工作。開發通過提交工單接入我們的數據系統。工單模型在設計之初只考慮到了一種類型的工單(新特征的申請),對于工單生命周期的每個節點分別用一個接口去實現。隨著業務迭代,還有一些操作也需要通過走工單讓管理員審批執行。此時最初的工單模型不能滿足需求,此時為了讓系統先用起來,我們的做法是寫單獨的接口去實現...這樣雖然能用,但是導致后端代碼里多出來了很多API。趁著過年前幾天業務不多,我對工單部分代碼進行了重構,希望達到的效果是后續不同類型的工單復用同一套工單流程,同時減輕前后端交互的成本。
需求分析
經過抽象,對于我們的系統不同類型的工單,工單的生命周期都是一樣的,工單只有這些狀態:
工單這幾個狀態要執行的操作差別是很大的,所以分別用不同接口去實現每一種工單狀態,這其中代碼的復用不多。工單狀態和執行操作如下圖:
前面說到,在系統之前的代碼里面不同類型的工單分別用不同的API實現,看代碼可以發現,不同類型的工單在生命周期的一個節點里面做的操作是類似的。比如對于新建工單,重構代碼之前操作是這樣:
增加工單種類之后,新建工單操作是這樣:
其中校驗前端參數、調用工單實例、發送通知的代碼都是可以復用的。只有工單操作這一塊行為有所區別,工單操作簡單抽象一下分為兩種:
實現思路
考慮到前端同學的開發成本,這次重構復用之前的接口,在每個接口參數里面增加一項工單類型(worksheetType),根據工單類型,做不同的操作。
重構的思路有兩種,一種是"函數式編程"(FP),另一種是"面向對象編程"(OOP)。這里曬出一張經典的圖片,hhh...
實現對比
為了對比兩種方式,分別實現了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地址
歡迎關注我的公眾號:薯條的自我修養