前段时间,应用结构器模式重构了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这样的数据结构还反对map
、filter
等链式操作,但很少人再提起链式调用,反而是隔壁Java用一个全新的词汇 Stream API,但Java的实现是通过Stream的形象接口,局限于实现Collection接口的数据结构。
直到起初专门去学习函数式编程,接触柯里化
等概念。惋惜Go 对函数式编程的反对十分个别,连最根本的箭头函数都不反对,所以不打算像Rust那样应用宏实现主动柯里化,退而求其次,抉择中规中矩的OOP
常见的结构器模式
,笨是笨点,但至多容易了解和保护。
type LogLevel = stringconst ( 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点供大家参考:
- 每个字段都抽取出一个
interface
,interface 包裹着setter
办法 - 通过管制
interface
外部的setter
办法返回值类型来管制办法的调用程序,比方UserInterface
的返回值是OperationInterface
- 对于类型是
枚举
的字段,比如说Level
字段的类型就是只有Debug
、Info
等无限个数的,能够别离创立别名办法,比方Debug()
、Info()
- 每个枚举类型的
setter
办法都能够额定设置一个非枚举的字段,比如说Debug(string)
可能同时设置Level
和Result
两个字段的值。 - 最初一个字段的
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
,也能够做到提早求值,具体做法是把中间状态保留到构造体中,但因为函数栈帧的大小无限,最终都会逃逸到堆上,因而性能不是最佳,默认也不是线程平安的,长处是易于了解。