本文转载自微信公众号《网管唠叨比唠叨》,作者不玩游戏的网管。转载本文请联系网管谢bi公众号。今天新开一个系列,讲一些实用的编程模型。学习完每个编程模型后,就可以马上应用到实战中。让我们写出更具表现力、易维护、易扩展、优雅的代码。.我会用Go来演示这些编程模式的例子,但其实这些模式大部分与语言无关,不管你平时关注Go、Java还是JavaScript,我觉得都可以用。为了避免贴出长代码,我会适当使用一些伪代码。理解思路后,可以在我的GitHub仓库gocookbook中找到完整的可运行代码。公众号回复gocookbook关键字获取链接,打开后按Ctrl+F搜索“Options”。本系列第一篇要分享的编程模式是函数式编程中的Options模式。Options模式解决了什么问题?Options模式可以使具有多个可选参数的函数或方法更整洁,更易于扩展。当一个函数有五个、六个甚至十多个可选参数时,使用这种模式的优势就会体现出来。显然,我们还是通过一些例子慢慢感受。比如我们要在项目中封装一个发送Http请求的通用工具函数,它的参数是什么?因为是工具函数,所以需要定义很多可以配置HTTP客户端的参数,比如:funcHttpRequest(methodstring,urlstring,body[]byte,headersmap[string]string,timeouttime.Duration)...返回此处省略了函数签名中的值。太宽了,影响阅读。请注意这里。如果上面的工具功能只是针对GET请求,很多HTTP客户端的设置是不需要设置的,我们一般会设置一个默认的超时时间。如果按照通常定义函数的方式来实现,函数逻辑中难免会有很多判断空值的逻辑。ifbody!=nil{//设置请求体Data...}ifheaders!=nil{//设置请求头...}调用时,调用者的代码要传一些零值给不需要的配置参数定制。HttpRequest('GET','https://www.baidu.com',nil,nil,2*time.Second)如果是Java的话其实可以通过方法重载来解决这个问题,但是如果是可选的就有参数有十几个,如果每个调用者对可选参数的顺序有不同的要求,定义这种多重重载方法显然不是一个好的解决方案。另一种常用的解决方案是在定义效用函数的签名时定义一个配置对象,而不是每个可能需要配置的可选参数。typeHttpClientConfigstruct{timeouttime.Durationheadersmap[string]stringbody[]byte}funcHttpRequest(methodstring,urlstring,config*HttpClientConfig)...配置对象方案的问题在函数签名中,传递一个配置对象来聚合各种可能的可选参数。这个方案,对于调用者来说,看起来比之前的方法简单多了。如果所有选项都是默认的,则只需将零值传递给配置对象的参数即可。HttpRequest('GET','https://www.baidu.com',nil)但是对于功能的实现者来说,判断那些选项参数的非零值还是必不可少的,因为配置对象可以在函数外被改变,这个有一定概率配置对象在函数内部使用之前会被外部程序改变。如果真的出现相关BUG,排查起来会很头疼。可变参数方案的问题类似于配置对象方案的问题。如果单纯通过可变参数来解决这个问题,会出现很多问题。funcHttpRequest(methodstring,urlstring,options...interface{})...虽然参数是可变的,但是实现者需要通过遍历来设置HTTP客户端的不同选项,让可变参数固定传递顺序,而caller如果要设置可选项,就得记住参数的顺序。您不能直接通过函数签名确定参数的顺序。看起来还不如我们最原始的方案。最后说一下如何使用Options模式来解决这个问题。其实如果你用过gRPC,你会发现gRPCSDK中的Options模式是有很大几率出现的。比如它的客户端方法可以通过以开头的闭包函数方法有很多。client.cc,err=grpc.Dial("127.0.0.1:12305",grpc.WithInsecure(),grpc.WithUnaryInterceptor(...),grpc.WithStreamInterceptor(...),grpc.WithAuthority(...))这些配置方法返回一个名为DialOption的接口。typeDialOptioninterface{apply(*dialOptions)}funcWithInsecure()DialOption{...}现在我们使用Options模式修改我们的工具功能,首先定义一个合约和配置对象。//可选的HTTP请求配置项,模仿gRPC使用的Options设计模式,实现typerequestOptionstruct{timeouttime.Durationdatastringheadersmap[string]string}typeOptionstruct{applyfunc(option*requestOption)}funcdefaultRequestOptions()*requestOption{return&requestOption{//默认请求Optiontimeout:5*time.Second,data:"",headers:nil,}}接下来我们定义配置函数,每个配置函数都会在请求配置对象中设置一定的配置。funcWithTimeout(timeouttime.Duration)*Option{return&Option{apply:func(option*requestOption){option.timeout=timeout},}}funcWithData(datastring)*Option{return&Option{apply:func(option*requestOption){option.data=data},}}然后我们的效用函数的签名应用上面定义的接口契约。funcHttpRequest(methodstring,urlstring,options...*Option)...在其实现中,我们只需要遍历options的可变参数,调用每个Option对象的apply方法来配置配置对象即可。更改参数的顺序。funchttpRequest(methodstring,urlstring,options...*Option){reqOpts:=defaultRequestOptions()//默认请求选项for_,opt:=rangeoptions{//应用reqOpts上options设置的选项opt.apply(reqOpts)}//创建请求对象req,err:=http.NewRequest(method,url,strings.NewReader(reqOpts.data))//设置请求头为key,value:=rangereqOpts.headers{req.Header.Add(key,value)}//发起请求...返回}总结最后,我们的HTTP工具函数的调用方式就变成了下面这种更加灵活和表达的方式。HttpRequest("GET",url)HttpRequest("POST",url,WithHeaders(headers)HttpRequest("POST",url,WithTimeout(超时),WithHeaders(headers),WithData(data))从实现上看?如果后面要在配置对象中添加其他的配置项,只需要扩展类型的字段,定义一个对应的With方法即可。扩展性完全在可以接受的范围内。好了,Options模式你学会了吗,想快速使用吗?快起来,现在在公众号回复关键字gocookbook,获取完整可运行的代码示例(记得打开链接后按Ctrl+F搜索“Options”),下次你遇到类似的场景,记得把今天学到的用起来。
