关于后端:从链式调用谈构造器模式与柯里化

2次阅读

共计 4894 个字符,预计需要花费 13 分钟才能阅读完成。

前段时间,应用结构器模式重构了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,也能够做到提早求值,具体做法是把中间状态保留到构造体中,但因为函数栈帧的大小无限,最终都会逃逸到堆上,因而性能不是最佳,默认也不是线程平安的,长处是易于了解。

正文完
 0