structの生成にfactory的な関数を用意しておくと埋め込みに変えたときに特に嬉しいという話
はじめに
structの生成にfactory的な関数を用意しておくと埋め込みに変えたときに特に嬉しいという話。これはもしかしたらニッチな話かもしれない。あるいはgoの本のどこにでも書いてある一般的な話かもしれない。今日goのコードを書いていてなるほどなーと思ったことだったので。備忘録として書いてみる。
はじめは1つのstructだった
はじめは1つのstructだった。例えば以下のようなstruct。何かしらのjobを模したものだと思って欲しい。
// Job : type Job struct { Name string Status Status RetryAt *time.Time CanceledAt *time.Time FinishedAt *time.Time } // Status : type Status int // Status : const ( Waiting Status = iota Running Canceled Done )
初めはwaiting状態のstatusがRunning状態になったあとDoneの状態になる。そういうようなstruct。
時には失敗することもある
常にジョブの実行が成功するわけではない。時には失敗することもある。そのようなときにはエラーの発生時刻やどのようなエラーが発生したかの情報がほしいかもしれない。LastErrorString
と ErrorAt
を追加してみる。例えばこのような感じ。
@@ -15,10 +15,12 @@ // Job : type Job struct { - Name string - Status Status - RetryAt *time.Time - CanceledAt *time.Time - FinishedAt *time.Time + Name string + Status Status + RetryAt *time.Time + CanceledAt *time.Time + FinishedAt time.Time + ErrorAt *time.Time + LastErrorString *time.Time }
何かしらの状態遷移のテストコードが増えてくる。例えばテスト用のstatus遷移。retryするときのretry間隔の計算などの処理が存在しうる。テストコードの中では以下のように普通にstructを手動で作成してfixtureとして使っているかもしれない(そうだということにする)。
package foo import ( "testing" ) func TestStateTransition(t *testing.T){ t.Run("testxxxx", t.Run(t *testing.T){ job := &Job{Status: Canceled} Revive(job) if job.Status != Status.Waiting { t.Errorf("after revived, status should be waiting, but %s", job.Status) } } t.Run("testxxxx2", t.Run(t *testing.T){ // ... } t.Run("testxxxx3", t.Run(t *testing.T){ // ... } t.Run("testxxxx4", t.Run(t *testing.T){ // ... } ... // table driven testになっているかもしれないけれど }
普通にテストがあって良い感じ。
少し変わった挙動を持つものが現れる
少し変わった挙動を持つものが現れる。例えば、しばらくの間だけ処理をスキップしてほしいというようなもの。(Status増やすというような話とは少しだけ違うようなことが必要になった)似たような振る舞いをする部分があるものの完全に同じものではないのでstructを分けたい(Alternativeというstructとする)。元のSkipをしないものと比べてみると状態遷移自体は同じだったので状態に関わる部分に関してはStateという構造体に分けると言うようなことをしたとする。
// State : type State struct { Status Status CanceledAt *time.Time FinishedAt time.Time ErrorAt *time.Time LastErrorString *time.Time } // Job : type Job struct { Name string State } // Alternative : type Alternative struct { Name string Condition string State Skipped bool }
ここからが本題。この埋め込みに変えた時に過去に書いたテストのコードも直す必要が出てくる。
テストコードの修正が必要になる。つらい。
テストコードの修正が必要になる。どういう修正が必要かと言うと埋め込みの対応。例えば以下のようにCanceledというStatusで作成していたfixtureを変える必要がある。
job := &Job{Status: Canceled}
Status部分は埋め込みに変えたので直す必要がある。以下のように。
job := &Job{State: {Status: Canceled}}
この種の変更を全てのfixtureへ行う必要がある。つらい。
factory的な関数が用意してあったなら
factory的な関数を用意してあったならそのような修正は不要。Factory関数というのは以下のようなもの。(可変長引数で更新用の関数を取るようなちょっとしfunctional optionっぽいインターフェイスにしておくとより便利)
// NewJob : func NewJob(fs ...func(*Job)) *Job { job := &Job{} for _, f := range fs { f(job) } return job }
何が良いのかと言うと、埋め込まれた型の生成はzero valueによる生成され、値への属性アクセスは自動的に埋め込まれた値に移譲される点。このおかげで、factory的な関数を経由した値の生成部分はコードを修正する必要がない。例えば以下の様な形になっているとしたら。このコードは埋め込みに変えた場合にも動く。
job := NewJob(func(job *Job) {
job.Status = Canceled
})
これは、実質以下の様に初期化された値を
job := &Job{State: State{}}
以下の様に触っていることに他ならないので。
job.State.Status = Waiting
そんなわけで、structの生成にfactory的な関数を用意しておくと埋め込みに変えたときに特に嬉しい。