[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地址
欢迎关注我的公众号:薯条的自我修养

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容