从链式调用谈构造器模式与柯里化

前段时间,使用构造器模式重构了http 工具类库,顺带优化一下AuditLog 模块的代码,然后很意外地被领导发现,他跟我说:这就是链式调用。这句话勾起我多年前的回忆

type LogLevel = string 
const (
    Debug LogLevel = "Debug" 
    Info LogLevel = "Info" 
    Warn LogLevel = "Warn" 
    Error LogLevel = "Error"
)

struct AuditLog {
    User string
    Operation string
    Level LogLevel
    Result string
    Timestamp time.Date
}

// 重构前 
sendAuditLog({
    User: "admin",
    Operation: "delete user",
    Result: "failed",
    Level: "Error",
    Timestamp: "2022-10-13"
})

// 重构后 
Builder().User("admin").Operation("delete user").Error("Failed")

早在十多年前,也就是2012年左右的光景,那时候前端还是jQuery的天下,大家非常熟悉的React、Angular和Vue三剑客还没兴起。我作为一个刚入学的大学生接触前端的第一个框架就是jQuery,那时候IE浏览器的份额还很高,jQuery抹平IE、Chrome和FireFox之间的差异,而且它特有的链式调用 更是操作DOM的一把利器。

$("selector").html("abcd")

但随着技术的演进,尤其是V8引擎的强大性能改进,使得Javascript从一个玩具语言逐渐成为最受欢迎的编程语言,像React、Angular和Vue这样的MVVM类库框架逐渐兴起,jQuery逐渐退出舞台。

const eventNumbers = \[1,2,3,4,5,6\].filter(num => num % 2 == 0)
    .map(item => \`${item} is event number\`)

虽然像Array、Map这样的数据结构还支持mapfilter等链式操作,但很少人再提起链式调用,反而是隔壁Java用一个全新的词汇 Stream API,但Java的实现是通过Stream的抽象接口,局限于实现Collection接口的数据结构。

直到后来专门去学习函数式编程,接触柯里化 等概念。可惜Go 对函数式编程的支持非常一般,连最基本的箭头函数都不支持,所以不打算像Rust那样使用宏实现自动柯里化,退而求其次,选择中规中矩的OOP 常见的构造器模式,笨是笨点,但至少容易理解和维护。

type LogLevel = string
const (
    Debug LogLevel = "Debug"
    Info LogLevel = "Info"
    Warn LogLevel = "Warn"
    Error LogLevel = "Error"
)

struct AuditLog {
    User string
    Operation string
    Result string
    Level LogLevel
    Timestamp Date
}

func (log *AuditLog) User(user string) OperationInterface {
    log.User = user
    return log
}

func (log *AuditLog) Operation(op string) LevelInterface {
    log.Operation = op
    return log
}

func (log *AuditLog) Debug(result string) *AuditLog {
    log.Level = Debug
    log.Result = result
    return log
}

func (log *AuditLog) Info(result string) *AuditLog {
    log.Level = Info
    log.Result = result
    return log
}

func (log *AuditLog) Warn(result string) *AuditLog {
    log.Level = Warn
    log.Result = result
    return log
}

func (log *AuditLog) Error(result string) *AuditLog {
    log.Level = Error
    log.Result = result
    return log
}

// 构造函数
func NewAuditLog() *AuditLog {
    return &AuditLog{
        User: "",
        Operation: "",
        Result: "",
        Level: Error,
        Timestamp: time.Now()
    }
}

// 构造器模式 
func Builder() UserInterface {
    return NewAuditLog()
}

// 针对User字段封装的setter函数 
interface UserInterface {
    User(string) OperationInterface
}

// 针对Operation字段封装的setter函数
interface OperationInterface {
    Operation(string)
}

// 针对Level字段封装的setter函数,对枚举进行特殊处理 
interface LevelInterface {
    Debug(string) *AuditLog
    Info(string) *AuditLog
    Warn(string) *AuditLog
    Error(string) *AuditLog
}

重构后的代码看起来更简洁且条理清晰,最重要的是看起来像符合自然语言的阅读习惯,这被称之为代码的 语义化

// 重构后 
Builder().User("admin").Operation("delete user").Error("Failed")

以上的重构过程需要遵守哪些规则或者诀窍,我列出来以下5点供大家参考:

  1. 每个字段都抽取出一个 interface ,interface 包裹着 setter 方法
  2. 通过控制 interface 内部的 setter 方法返回值类型来控制方法的调用顺序,比如 UserInterface 的返回值是 OperationInterface
  3. 对于类型是枚举的字段,比如说 Level 字段的类型就是只有 DebugInfo等有限个数的,可以分别创建别名方法,比如 Debug()Info()
  4. 每个枚举类型的 setter 方法都可以额外设置一个非枚举的字段,比如说 Debug(string) 能够同时设置 LevelResult 两个字段的值。
  5. 最后一个字段的 setter 方法要返回完整的结构体,或者被特定interface包裹起来的结构体,目的是隐藏实现细节。

以上过程非常公式化,完全可以通过代码自动生成,然后再后期人为调整一下顺序,使得方法调用顺序更适合阅读习惯。

我认为比较值得讨论的点,就是使用 interface 代替原来的 枚举,让代码看起来更简洁易懂且语义化。

围绕着 使用 interface 代替 枚举 的话题,我想进一步探讨 Go 语言中如何实现复杂类型枚举

由于Go语言的限制,Go语言中的枚举都是通过数字、字符串等基本类型来模拟的,不存在真正意义上的枚举。

要实现复杂结构的枚举,往往需要借助全局变量,但是全局变量并不是只读,在运行过程中容易被篡改,存在数据竞争的风险。

struct ResponseEnum {
    Code int
    MsgEn string
    MsgCn string
    Extra Extra
}

struct Extra {
    StatusCode string
    Message string
}

var Succeed = ResponseEnum {
    Code: 200,
    MsgEn: "request succeed",
    MsgCn: "请求成功"
}
var BadRequest = ResponseEnum {
    Code: 400,
    MsgEn: "invalid request",
    MsgCn: "请求参数有误"
}
var Unauthorized = ResponseEnum {
    Code: 401,
    MsgEn: "needs login",
    MsgCn: "需要登陆"
}
var NotFound = ResponseEnum {
    Code: 401,
    MsgEn: "the requested resource is not existed",
    MsgCn: "资源不存在"
}

使用 interface 替换 枚举 重构:

interface Response {
    Code() int
    MsgEn() string
    MsgCn() string
    Extra() *Extra
}

struct httpResponse {
    Code int `json: "code"`
    MsgEn string `json: "msgEn"`
    MsgCn string `json: "msgCn"`
    Extra Extra `json: "extra"`
}

func (resp *httpResponse) Code() int {
    return resp.Code
}

func (resp *httpResponse) MsgEn() string {
    return resp.MsgEn
}

func (resp *httpResponse) MsgCn() string {
    return resp.MsgCn
}

func (resp *httpResponse) Extra() Extra {
    return resp.Extra
}

func Succeed(extra Extra) Response {
    return &httpResponse {
        Code: 200,
        MsgEn: "request succeed",
        MsgCn: "请求成功", 
        Extra: extra
    }
}

func BadRequest(extra Extra) Response {
    return &httpResponse {
        Code: 400,
        MsgEn: "invalid request",
        MsgCn: "请求参数有误" 
        Extra: extra
    }
}

func Unauthorized(extra Extra) Response {
    return &httpResponse {
        Code: 401,
        MsgEn: "needs login",
        MsgCn: "需要登陆" 
        Extra: extra
    }
}

func NotFound(extra Extra) Response {
    return &httpResponse {
        Code: 401,
        MsgEn: "the requested resource is not existed",
        MsgCn: "资源不存在" 
        Extra: extra
    }
}

重构完后,原本的全局变量都变成公开函数,每次调用函数都会返回一个全新结构体示例,自然就不会存在数据竞争的场景。

而且返回的结果类型都是 interface ,只提供读取操作屏蔽修改操作,消除运行中篡改数据的可能,提高代码的健壮性。

并且可以通过创建 函数别名 的方式自定义枚举:


// DuplicatedName 所有场景都通用的自定义重名资源错误枚举
func DuplicatedName() Response {
    return BadRequest(Extra{StatusCode: "CustomizedCode001", Message: "资源名称已存在"})
}

// ImageError 跟镜像相关的错误枚举
func ImageError(code string, msg string) Response {
    // 状态码统一加上IMG前缀
    return BadRequest(Extra{StatusCode: "IMG" + code, Message: msg})
}

总结

链式调用 Chain Call 更像是一种俗称,它常见的实现方式有两种:CPS(Continuation Passing Style) 和 State Machine两种,
前者是函数式的柯里化 Currying ,一般都是延迟求值 lazy evaluation,具体做法是把中间状态保存到函数栈帧中,默认是线程安全的;
后者是面向对象的状态机 State Machine ,也可以做到延迟求值,具体做法是把中间状态保存到结构体中,但因为函数栈帧的大小有限,最终都会逃逸到堆上,因此性能不是最佳,默认也不是线程安全的,优点是易于理解。

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

推荐阅读更多精彩内容