非功能性需求代码与功能性需求代码的 “楚河汉界”(Golang)

1,099 阅读3分钟
原文链接: zhuanlan.zhihu.com

非功能性需求代码,和功能性需求的实现代码中间要画一根线。这样当我们去理解业务逻辑的时候不会被非功能性需求的代码所干扰。而非功能性需求的实现要升级也会比较容易,不用去到处改代码。

这里的边界包括:

  • 对外暴露成可调用的服务器
  • 通过客户端调用外部的服务
  • 用expvar或者日志进行埋点(提供 observability)
  • 集群管理通道(注册和管理后门)
  • 支持推送更新的配置缓存

v2pro/plz 按照这个边界划分,给 Go 的代码提供了一个比标准库更高一级的基础 api/spi。起到的作用就是给分布式的服务开发提供一个标准库。在这些边界中,以服务器和客户端的边界最难以画清楚。

比较理想的接口是这样的

type MyRequest struct {
  // ...
}
type MyResponse struct {
  //  ...
}
func sayHello(ctx *countlog.Context, req *MyReqeust) (*MyResponse, error) {
	// ...
} 

功能性需求代码只需要关注我对外提供的服务是这样的一个接口。至于是暴露在了什么tcp端口上,用的是 http/JSON 还是 thrift,是否有限流,这些都是非功能性需求的代码和配置需要关注的事情。

利用 v2pro/plz.service 我们可以把接口定义成这样的了。从而把楚河汉界划分得一清二楚。

如果是启动成 http 服务,则是这样的

func sayHello(ctx *countlog.Context, req *MyReqeust) (*MyResponse, error) {
	// ...
}
server := http.NewServer()
server.Handle("/sayHello", sayHello)
server.Start("127.0.0.1:9998")

如果是启动成 thrift 服务,则是这样的

func sayHello(ctx *countlog.Context, req *MyReqeust) (*MyResponse, error) {
	// ...
}
server := thrift.NewServer(thrifter.Config{Protocol: thrifter.ProtocolBinary, IsFramed: true}.Froze())
server.Handle("sayHello", sayHello)
server.Start("127.0.0.1:9998")

我们可以看到,参数绑定这样的事情从业务代码里划走了,而且一份代码可以同时暴露成 http 服务和 thrift 服务了。

对于客户端接口,也是一样的。我们的代码里依赖的客户端定义成这样:

var sayHello = func (ctx *countlog.Context, req *MyReqeust) (*MyResponse, error)

使用的时候,就把 sayHello 当成一个函数来使用就行了。至于这个函数调用的背后是 http 服务,还是 thrift 服务,是走了服务发现还是固定ip端口,是有负载均衡还是没有,这些都是非功能性需求。

比如,利用 v2pro/plz.service 调用 http 服务

var sayHello = func (ctx *countlog.Context, req *MyReqeust) (*MyResponse, error)
client := http.NewClient()
client.Handle("POST", "http://127.0.0.1:9998/sayHello", &sayHello)

// use sayHello(...) to call server

或者调用 thrift 服务

var sayHello = func (ctx *countlog.Context, req *MyReqeust) (*MyResponse, error)
client := thrift.NewClient(thrifter.Config{Protocol: thrifter.ProtocolBinary, IsFramed: true}.Froze())
client.Handle("127.0.0.1:9998", "sayHello", &sayHello)

// use sayHello(...) to call server

通过不同的 client,给 sayHello 这个函数指针绑定了不同的实现。对于功能性需求的代码来说,无论你外面怎么升级,都不会影响业务逻辑的写法。

从内部实现的角度来说。这里的 client/server 都没有使用 reflect 来进行函数调用,省去了反射的开销。无论是 client 还是 server 的 handler,在内部实现里都是转换成同一个函数签名来调用的:

// Handler is the function prototype for both client and server.
// User should substitute request and response with their own concrete types.
// For example func(ctx *countlog.Context, request NewOrderRequest) (NewOrderResponse, error)
type Handler func(ctx *countlog.Context, request unsafe.Pointer) (response unsafe.Pointer, err error)