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