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という名前の型は、一連の関数の抽象型であり、これらの関数の共通の特徴は、
// 1つのstring引数を受け取り、1つの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データを設定します。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 日].
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。