GO 編程模式系列(三):FUNCTIONAL OPTIONS

原文作者:陳皓(左耳朵耗子)

內容出

https://coolshell.cn/articles/21146.html

在本篇文章中,我們來討論一下Functional Options這個編程模式。這是一個函數式編程的應用案例,編程技巧也很好,是目前在Go語言中最流行的一種編程模式。但是,在我們正式討論這個模式之前,我們需要先來看看要解決什麼樣的問題。

本文是全系列中第3 / 9篇:Go編程模式

  • Go編程模式:切片,接口,時間和性能

  • Go 編程模式:錯誤處理

  • Go 編程模式:Functional Options

  • Go編程模式:委託和反轉控制

  • Go編程模式:Map-Reduce

  • Go 編程模式:Go Generation

  • Go編程模式:修飾器

  • Go編程模式:Pipeline

  • Go 編程模式:k8s Visitor 模式

目錄

  • 配置選項問題

  • 配置對象方案

  • Builder模式

  • Functional Options

  • 參考文檔

配置選項問題

在我們編程中,我們會經常性的需要對一個對象(或是業務實體)進行相關的配置。比如下面這個業務實體(注意,這僅只是一個示例):

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 構造,的確是這樣的。但是在處理錯誤的時候可能就有點麻煩(需要爲Server結構增加一個error 成員,破壞了Server結構體的“純潔”),不如一個包裝類更好一些。

如果我們想省掉這個包裝的結構體,那麼就輪到我們的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

(全文完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章