GO 程式設計模式03:FUNCTIONAL OPTIONS
GO 程式設計模式:FUNCTIONAL OPTIONS
在本篇文章中,我們來討論一下 Functional Options 這個程式設計模式。這是一個函數語言程式設計的應用案例,程式設計技巧也很好,是目前在Go語言中最流行的一種程式設計模式。但是,在我們正式討論這個模式之前,我們需要先來看看要解決什麼樣的問題。
配置選項問題
在我們程式設計中,我們會經常性的需要對一個物件(或是業務實體)進行相關的配置。比如下面這個業務實體(注意,這僅只是一個示例):
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
TLS *tls.Config
}
在這個 Server
物件中,我們可以看到:
- 要有偵聽的IP地址
Addr
和埠號Port
,這兩個配置選項是必填的(當然,IP地址和埠號都可以有預設值,當這裡我們用於舉例認為是沒有預設值,而且不能為空,需要必填的)。 - 然後,還有協議
Protocol
、Timeout
和MaxConns
欄位,這幾個欄位是不能為空的,但是有預設值的,比如:協議是tcp
, 超時30
秒 和 最大連結數1024
個。 - 還有一個
TLS
這個是安全連結,需要配置相關的證書和私鑰。這個是可以為空的。
所以,針對於上述這樣的配置,我們需要有多種不同的建立不同配置 Server
的函式簽名,如下所示(程式碼比較寬,需要左右滾動瀏覽):
func NewDefaultServer(addr string, port int) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, 100, nil}, nil
}
func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil
}
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
return &Server{addr, port, "tcp", timeout, 100, nil}, nil
}
func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
return &Server{addr, port, "tcp", 30 * time.Second, maxconns, tls}, nil
}
因為Go語言不支援過載函式,所以,你得用不同的函式名來應對不同的配置選項。
配置物件方案
要解決這個問題,最常見的方式是使用一個配置物件,如下所示:
type Config struct {
Protocol string
Timeout time.Duration
Maxconns int
TLS *tls.Config
}
我們把那些非必輸的選項都移到一個結構體裡,於是 Server
物件變成了:
type Server struct {
Addr string
Port int
Conf *Config
}
於是,我們只需要一個 NewServer()
的函數了,在使用前需要構造 Config
物件。
func** NewServer(addr string, port int, conf *Config) (*Server, error) {
//...
}
//Using the default configuratrion
srv1, _ := NewServer("localhost", 9000, nil)
conf := ServerConfig{Protocol:"tcp", Timeout: 60*time.Duration}
srv2, _ := NewServer("locahost", 9000, &conf)
這段程式碼算是不錯了,大多數情況下,我們可能就止步於此了。但是,對於有潔癖的有追求的程式設計師來說,他們能看到其中有一點不好的是,Config
並不是必需的,所以,你需要判斷是否是 nil
或是 Empty – Config{}
這讓我們的程式碼感覺還是有點不是很乾淨。
Builder模式
如果你是一個Java程式設計師,熟悉設計模式的一定會很自然地使用上Builder模式。比如如下的程式碼:
User user = new User.Builder()
.name("Hao Chen")
.email("[email protected]")
.nickname("左耳朵")
.build();
仿照上面這個模式,我們可以把上面程式碼改寫成如下的程式碼(注:下面的程式碼沒有考慮出錯處理,其中關於出錯處理的更多內容,請參看《Go 程式設計模式:出錯處理》):
//使用一個builder類來做包裝
type ServerBuilder struct {
Server
}
func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
sb.Server.Addr = addr
sb.Server.Port = port
//其它程式碼設定其它成員的預設值
return sb
}
func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
sb.Server.Protocol = protocol
return sb
}
func (sb *ServerBuilder) WithMaxConn( maxconn int) *ServerBuilder {
sb.Server.MaxConns = maxconn
return sb
}
func (sb *ServerBuilder) WithTimeOut( timeout time.Duration) *ServerBuilder {
sb.Server.Timeout = timeout
return sb
}
func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder {
sb.Server.TLS = tls
return sb
}
func (sb *ServerBuilder) Build() (Server) {
return sb.Server
}
於是就可以以如下的方式來使用了
sb := ServerBuilder{}
server, err := sb.Create("127.0.0.1", 8080).
WithProtocol("udp").
WithMaxConn(1024).
WithTimeOut(30*time.Second).
Build()
上面這樣的方式也很清楚,不需要額外的Config類,使用鏈式的函式呼叫的方式來構造一個物件,只需要多加一個Builder類,這個Builder類似乎有點多餘,我們似乎可以直接在Server
上進行這樣的 Builder 構造,的確是這樣的。但是在處理錯誤的時候可能就有點麻煩,不如一個包裝類更好一些。
如果我們想省掉這個包裝的結構體,那麼就輪到我們的Functional Options上場了,函數語言程式設計。
Functional Options
首先,我們先定義一個函式型別:
type Option func(*Server)
然後,我們可以使用函式式的方式定義一組如下的函式:
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConns(maxconns int) Option {
return func(s *Server) {
s.MaxConns = maxconns
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
上面這組程式碼傳入一個引數,然後返回一個函式,返回的這個函式會設定自己的 Server
引數。例如:
- 當我們呼叫其中的一個函式用
MaxConns(30)
時 - 其返回值是一個
func(s* Server) { s.MaxConns = 30 }
的函式。
這個叫高階函式。在數學上,就好像這樣的數學定義,計算長方形面積的公式為: rect(width, height) = width * height;
這個函式需要兩個引數,我們包裝一下,就可以變成計算正方形面積的公式:square(width) = rect(width, width)
也就是說,squre(width)
返回了另外一個函式,這個函式就是rect(w,h)
只不過他的兩個引數是一樣的。即:f(x) = g(x, x)
好了,現在我們再定一個 NewServer()
的函式,其中,有一個可變引數 options
其可以傳出多個上面上的函式,然後使用一個for-loop來設定我們的 Server
物件。
func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
MaxConns: 1000,
TLS: nil,
}
for _, option := range options {
option(&srv)
}
//...
return &srv, nil
}
於是,我們在建立 Server
物件的時候,我們就可以這樣來了。
s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))
怎麼樣,是不是高度的整潔和優雅?不但解決了使用 Config
物件方式 的需要有一個config引數,但在不需要的時候,是放 nil
還是放 Config{}
的選擇困難,也不需要引用一個Builder的控制物件,直接使用函數語言程式設計的試,在程式碼閱讀上也很優雅。
所以,以後,大家在要玩類似的程式碼時,強烈推薦使用Functional Options這種方式,這種方式至少帶來了如下的好處:
- 直覺式的程式設計
- 高度的可配置化
- 很容易維護和擴充套件
- 自文件
- 對於新來的人很容易上手
- 沒有什麼令人困惑的事(是nil 還是空)
參考文件
- “Self referential functions and design” by Rob Pike
http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html
(全文完)
轉載:文章作者和出處 酷 殼 – CoolShell
關注CoolShell微信公眾賬號和微信小程式