simon

simon

github

Functional Options Pattern デフォルト引数のGo関数

Functional Options Pattern: デフォルト引数のある Go 関数#

Created: 2021 年 5 月 4 日 14:32
Tags: Golang

1. 問題#

Golang を使用していると、Python に比べていくつかの構文糖が欠けていることに気づきます。例えば、関数の引数にはデフォルト値を持たせることができ、デフォルト値のある引数は省略可能です。Java にはこのような構文糖はありませんが、オーバーロードを使用して類似の効果を実現できます。

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

exsample("hello")
出力: hello world

exsample("hello", "friends")
出力: hello friends

今日は UT を書く必要があり、データを構築する必要があることに気づきました。私たちのデータ構築方法には 2 つの選択肢があります。

  1. ORM を直接使用してインスタンスを作成し、パラメータ値をカスタマイズする
  2. ファクトリを作成し、faker を使用してデータを生成し、一部を指定し、一部を fake データを使用する

これらの 2 つの方法には明らかな欠点があります。

方法 1 では、毎回すべてのフィールドを構築する必要があり、非常に面倒です。

方法 2 では、インスタンスの引数をカスタマイズできず、関連する 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 クロージャ#

  • 概念

    クロージャは、関数とその環境を一緒に保存するレコードです。
    クロージャは、関数と関連する参照環境の組み合わせで構成されるエンティティです。

    • 関数は外部関数と内部関数を指します
      • 外部関数は環境を包むためにあり、環境を持つクロージャ関数を構築するためのものです
      • 内部関数は実際に実行される関数、つまりクロージャ関数自体です
      • クロージャ関数は実行可能な関数(内部関数)を返します
    • 環境
      • 環境とは、外部関数が渡す引数の値や内部の引数を指し、クロージャ関数内に保存され、内部関数の計算に使用されます
    • 同じクロージャ関数を複数回呼び出すと、それぞれが独立します

    この概念は 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)
    // 出力:foo val = 3
    

Go 関数の可変数引数#

  • Go 関数には可変引数があり、同じ型の不定数量の引数を渡すことができます。Go の print 関数はこの特性を利用しています。関数の引数はさまざまな型を取ることができ、カスタム型(例えば関数型)も含まれます。
  • 表示方法は以下の通りです。
// ...は不定数量のstring引数を渡すことができることを示します
func test (v1 string, ...vs string) {
    fmt.Println(v1)
    for _, s := range vs {
       fmt.Println(s)
    }
}

使用例は以下の通りです。

test("A")
出力: "A"
test("A", "B", "C")
出力: "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. コードを見せて!#

// wechat関連.
// 型を定義し、関数を使用して値を渡します。クロージャを使用して値を保証するため、値を渡す必要はありません。
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データを設定します。factory内のfakeパラメータです。
	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. もう一つのこと#

私たちはこの知識を学び、確かにこのものが素晴らしいことを理解していますが、それが私たちが使用することを意味するわけではありません。実際、私たちの多くの仲間がいくつかの場所で使用していることに気づきましたが、必要なすべての場所で使用しているわけではありません。

その理由は、これが非常に面倒で、多くのコードを書く必要があるからです。Python では、単に引数名を設定し、デフォルト値を設定するだけで済みますが、Go では同じ機能を実現するために多くのコードを書く必要があり、明らかに私たちにはその時間がありません。それに、これに時間を費やす価値はありません。これは性能の代償と言えるでしょう。しかし、両立する方法はあるのでしょうか?あります。

Generate-function-opts コマンド#

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

参考文献#

  1. Golang.org. 2021. The Go Programming Language Specification - The Go Programming Language. [online] Available at: https://golang.org/ref/spec#Function_types [Accessed 2021 年 2 月 25 日].
  2. talks, C., 2018. Go 関数選択パターン. [online] Code talks. Available at: https://lingchao.xin/post/functional-options-pattern-in-go.html [Accessed 2021 年 2 月 25 日].
  3. Sohamkamani.com. 2019. [online] Available at: https://www.sohamkamani.com/golang/options-pattern/ [Accessed 2021 年 2 月 26 日].
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。