simon

simon

github

Functional Options Pattern 缺省傳參的Go函數

功能選項模式:缺省傳參的 Go 函數#

Created: May 4, 2021 2:32 PM
Tags: Golang

1. 問題#

在使用 Golang 的過程中,發現相對於 Python 它缺少一些語法糖,例如函數的參數可以有默認值,有默認值的參數可以不傳。Java 雖然沒有這種語法糖,但是他有重載可以實現類似的效果

exsample

def exsample(must_have, option_value = "world"):
    print("{} {}".format(must_have + option_value))

exsample("hello")
output: hello world

exsample("hello", "friends")
output: hello friends

今天寫 UT,需要構造數據,發現我們的構造數據方式有兩種

  1. 直接使用 ORM 創建一個實例,參數值自定義
  2. 寫一個 factory,用 faker 造一些數據,部分指定,部分使用 fake 的數據

這兩個方法都有比較明顯的缺陷

方法 1,每次都要構造所有的字段,很麻煩

方法 2,無法自定義實例的參數,在我們構造相關聯的兩個實例時,無法指定關聯字段

2. 期望#

那麼,有沒有既可以最大限度的使用 faker 來造數據,同時又可以方便的自定某些參數的值進行關聯呢?我們來看下 Python 裡的 factory boy 是怎麼做的

class ModelAFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = ModelA

    name = factory.Sequence(lambda n: "ModelA%s" % n)
    description = factory.fuzzy.FuzzyText()
    pic = factory.Sequence(lambda n: "image%s" % n)
    status = Status.ACTIVE.value

    @factory.post_generation
    def related_models(obj, create, extracted, **kwargs):
        RelatedModelFactory.create(model_a=obj, **kwargs)

ModelAFactory 在 meta 中定義了 Model,然後自定義了其中 4 個字段的生成方式,其他字段用默認方式生成。

使用時,一種是完全使用 factory 造的參數,另一種是可以指定部分字段的值。例子我舉的是 name,更常見的情況是關聯其他表的字段。

instance_a = ModelAFactory()  # 完全依賴factory
instance_b = ModelAFactory(name="option")  # 自定了instance name的值

3. 方案及實現#

3.1 背景知識#

Function types 函數類型 [1]#

  • 自定義一個類型 (type),代表一系列類似的函數的集合 (func)
  • 這些函數有相同的輸入和輸出
// 名字為TypeName的類型,是一系列函數的抽象類型,這些函數的共同特點是,
// 接收一個string參數,返回一個string參數
type TypeName func(var1 string) string

// 給TypeName類型添加一個方法say,所有TypeName的類型的函數都會有這個方法函數
func (typeName TypeName) say(n string) {
    fmt.Println(typeName(n))  // 打印出某個TypeName類型的函數在傳入參數n後的返回值
}

// 我們來實現一個TypeName類型的函數方法
func english(name string) string {
    return "Hello, " + name
}

func french(name string) string {
    return "Bonjour, " + name
}

func main() {
    // 將english函數轉換為TypeName類型,這樣給english添加了一個方法say()
    g := TypeName(english)
    g.say("World")
    g = Greeting(french)
    g.say("World")
}

Closure 閉包#

  • 概念

    a closure is a record storing a function together with an environment.
    閉包是由函數和與其相關的引用環境組合而成的實體 。

    • 函數是指外部函數和內部函數
      • 外部函數主要是為了包裹環境,主要是為了構造一個帶有環境的閉包函數
      • 內部函數是實際需要執行函數,也就是閉包函數本身
      • 閉包函數返回一個可執行的函數(內部函數)
    • 環境
      • 所謂環境,其實就是指外部函數傳入的參數值,或者內部的參數,會被保存在閉包函數中,並且可以運用到內部函數的計算
    • 同一個閉包函數,多次調用,會各自獨立

    這個概念在 Go 裡面內容較多,這裡只介紹足夠我們實現目前功能的內容

  • 閉包函數例子

    func foo(x int) func() {
        return func(y int) {
            fmt.Printf("foo val = %d\n", x + y)
        }
    }
    
    // 初始化一個閉包函數, 1賦值給了foo的參數x,返回一個閉包函數(也就是帶有x值的 func(y int))
    f1 := foo(1)
    // 執行閉包函數, y=2, x=1, 執行func中的匿名函數
    f1(2)
    // output:foo val = 3
    

Go 函數可變數量入參#

  • Go 函數有變參,即可以傳入不定數量的同類型參數,go 的 print 函數就是利用這個特性。函數的入參可以是各種類型,包括自定類型,例如 function types
  • 表示方式如下
// ...就表示有未數量的string參數可以傳入
func test (v1 string, ...vs string) {
    fmt.Println(v1)
    for _, s := range vs {
       fmt.Println(s)
    }
}

使用起來,如下

test("A")
Out: "A"
test("A", "B", "C")
Out: "A"
     "B"
     "C"

3.2 方案#

Functional Options Pattern 函數選擇模式 [2] [3]。

有了前面的知識,我們就可以來了解本文的主角 — 函數選擇模式,它通過函數的不同參數的變化,修改或增加函數本身的功能。

3.2.1 例子#

h := NewHouse(
   WithConcrete(),
   WithoutFireplace()
)

NewHouse 在這裡是一個構造函數,WithConcrete()WithFireplace()是構造函數的可選參數,這些參數可以修改函數的返回值。

3.2.2 定義構造函數#

// 定義一個構造體,裡面存放我們可選參數的值
type User struct {
    Name     string
}

// 構造函數, 默認名為張三
func NewUser() User {
    options := User{}
    options.Name = "張三"
    return options
}

3.2.3 定義函數選項#

外部函數 WithName 傳入 Name 字段對應想要自定義的值 name,返回一個 UserOption 類型的函數,也就是 return 的那個匿名函數。函數的作用是將 name 的參數值複製到,傳入可選參數結構體的指針的對應字段 Name,這樣賦值後,函數外的 * User 值也會修改。

//  定義一個自定義的函數類型,作為閉包函數的返回
//  入參為可變參數struct的指針,因為需要修改閉包外的值作為存儲
type UserOption func(options *User)

// 創建一個選項函數
func WithName(name string) UserOption {
	return func(options *User) {
		options.Name = name
	}
}

3.2.4 將函數選項添加到構造函數#

此時我們需要修改一下之前的構造函數,讓其支持傳入多個函數選項。"...UserOption" 表示可以傳入未知數量的 UserOption 類型的函數

func NewUser(opts ...UserOption) User {
	options := User{}
	options.Name = "張三"
  // 循環執行選項函數,進行字段的賦值
	for _, o := range opts {
		o(&options)
	}
	return options
}

使用,注意下,WithName 這個函數其實用閉包生成一個帶有可賦值參數值的 UserOption 類型函數,然後構造函數只要將存儲值的 struct 傳入執行一下,就可以把裡面的值替換掉

func main() {
	fmt.Println("Hello, playground")
  // 使用默認值
	user := NewUser()
	fmt.Println("Name", user.Name)
  // 使用自定義值
	user2 := NewUser(WithName("黃子龍"))
	fmt.Println("Name", user2.Name)
}

4. Show me the code!#

// wechat 相關.
// 定義一個類型,可以是函數,傳入值是我們的model,因為使用閉包保證賦值,所以沒有傳出.
type WechatOption func(options *entschema.WeChat)

// 用閉包進行自定義賦值.
func WithAppIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.AppID = s
	}
}

func WithUnionIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.UnionID = &s
	}
}

func WithOpenIDWechat(s string) WechatOption {
	return func(options *entschema.WeChat) {
		options.OpenID = s
	}
}

func WithAccountIDWechat(s uint64) WechatOption {
	return func(options *entschema.WeChat) {
		options.AccountID = s
	}
}

func WithTypeWechat(s wechat.Type) WechatOption {
	return func(options *entschema.WeChat) {
		options.Type = s
	}
}

func WeChatFactory(suite TestSuite, opts ...WechatOption) entschema.WeChat {
	options := entschema.WeChat{}
	// 配置默認的fake數據, fake參數在factory內.
	suite.NoError(faker.FakeData(&options))
	// 傳參則使用傳入的參數進行賦值,實現可變傳參.
	for _, o := range opts {
		o(&options)
	}
	aWechat := app.Database.WeChat.Create().
		SetAppID(options.AppID).
		SetOpenID(options.OpenID).
		SetAccountID(options.AccountID).
		SetUnionID(*options.UnionID).
		SetType(options.Type).
		SaveX(suite.Context())
	return *aWechat
}

wechatFactory(WithUnionIDWechat("id"))

5. One More Thing#

雖然我們學習了這個知識,也確實知道這個東西很不錯,但不代表我們會去用。我注意到其實我們不少小夥伴其實有在一些地方用到,但並非所有需要的地方都用到了。

究其原因,還是因為這太麻煩了,要寫很多代碼,相比於 python 只要定一個參數名,再設置一個默認值的方式。Go 需要實現同樣的功能要寫多得多的代碼,很明顯,我們沒有這個時間。它也不值得我們花時間這些時間。這算是性能的代價吧。但有沒有雙全法呢?有的

Generate-function-opts Command#

./generate-function-opts 
   -definitionFile gen/entschema/workwechatuser.go   
   -structTypeName WorkWeChatUser 
   -outFile pkg/testutils/workwechatuserfactory.go  
   -skipStructFields Edges

reference#

  1. Golang.org. 2021. The Go Programming Language Specification - The Go Programming Language. [online] Available at: https://golang.org/ref/spec#Function_types [Accessed 25 February 2021].
  2. talks, C., 2018. Go 函數選項模式. [online] Code talks. Available at: https://lingchao.xin/post/functional-options-pattern-in-go.html [Accessed 25 February 2021].
  3. Sohamkamani.com. 2019. [online] Available at: https://www.sohamkamani.com/golang/options-pattern/ [Accessed 26 February 2021].
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。