Go における FunctionalOptionPattern と MethodChaining について考える

きっかけ

この記事を書くきっかけは以下のブログで、
MethodChaining の代わりに FunctionalOptionPattern を利用したという記事。
https://www.calhoun.io/using-functional-options-instead-of-method-chaining-in-go/

この記事は @ono_matope さんのツイートで知った。




このツイートにリツイートした結果、
@hnakamur2 さんも加わり、3人でちょっとした議論をした。
議論の内容については以下を追ってもらえればと思う。
https://twitter.com/ono_matope/status/967788305830985728
*本体同士で面識もない方々とこういった議論ができるあたり、ツイッターって便利ですね。

せっかくなので、
上記ツイッター上でのやりとりを整理して、
自分なりの結論を書いてみようと思う。


FunctionalOptionPattern

これについては特に何も言及することはない。

FunctionalOptionPattern については以下を読むといい。
https://qiita.com/weloan/items/56f1c7792088b5ede136

この記事によると原典は以下なので、2014年からあったパターンみたい。
https://commandcenter.blogspot.jp/2014/01/self-referential-functions-and-design.html


MethodChaining

ここからが本題。

以下の記事は golang で MethodChaining の代わりに FunctionalOptionPattern を利用したという内容になっている。
https://www.calhoun.io/using-functional-options-instead-of-method-chaining-in-go/

ここからは自分の考えも交えて上記の記事を説明していくので、
記事中に載っていない内容を書いたり、
載っている内容を書かなかったりするかもしれません。
元記事を読んでから読み進めた方がいいと思います。


MethodChaining の問題点

なぜ MethodChaining の代わりに FunctionalOptionPattern を利用したかと言うと、
golang の戻り値でエラーを返すという言語仕様とMethodChainingの相性が悪いから。

java などの例外が存在する言語であれば、以下のようにメソッドをチェインすることができる。

User user = new User.Builder()
  .name("Michael Scott")
  .email("michael@dundermifflin.com")
  .role("manager")
  .nickname("Best Boss")
  .build();



しかし、golang の場合は戻り値でエラーを返すのでメソッドをチェインできない。
以下のように毎回エラーチェックをする必要がある。

var db *gorm.DB
var err error

db, err = db.Where("id = ?", 123)
if err != nil { ... }

db, err = db.Where("email = ?", "jon@calhoun.io")
if err != nil { ... }



このように golang のエラーハンドリングの言語仕様と MethodChaining の相性が悪いという点が問題となっている。


Error フィールドによる解決方法

golang の言語仕様と MethodChaining の相性が悪いという点はあるものの、
struct に Error フィールドを持たせることで、
それっぽく実装することができる。

記事中で例として扱われていたのは GORM 。

GORM は gorm.DB という struct に Error フィールドを持たせ、
MethodChainingによるエラーを格納しているらしい。
https://github.com/jinzhu/gorm/blob/48a20a6e9f3f4d26095df82c3337efec6db0a6fc/main.go#L15

MethodChainingが完了したタイミングで、
DB.Error が nil かどうかをチェックすることで、
エラーを補足できるようになっている。
http://gorm.io/docs/error_handling.html#Error-Handling

DB.Error のおかげで各メソッドで error を返す必要がなく、
メソッドをチェインすることができる。

if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
  // error handling...
}



Error フィールドによる解決方法の問題

しかし、Error フィールドによる解決方法にも問題がある。

1. 各メソッドでエラーが発生しないような印象を受ける

DB.Error がエラーを格納するので、
チェイン対象の各メソッドは error を返さない。

そのため、そのメソッドに対してエラーにならない操作のような印象を受ける。
しかし、実際はエラーが発生する可能性がある。

おおげさに言うと、
エラーが発生するという危機感が薄れてしまう。


2. エラーチェックを忘れそう その1

golang のエラーハンドリングは戻り値でエラーを補足するので、
MethodChaining後に DB.Error のエラーチェックをするのを忘れそう。


3. エラーチェックを忘れそう その2

gorm は Open() する際は error を返す。

func Open(dialect string, args ...interface{}) (db *DB, err error) {
	//省略
}

https://github.com/jinzhu/gorm/blob/48a20a6e9f3f4d26095df82c3337efec6db0a6fc/main.go#L44

しかし、DB.Error のエラーチェックも必要なので、
「DB.Error には、gorm.Open() 時のエラーも格納されるはず」という思い込みがあると、
gorm.Open() 時に発生するエラーをハンドリングせず、
結果的に不具合につながる。

逆に DB.Error のチェックが不要な場合でもチェックするようなコードを書いてしまうこともある。

ちなみに、
gorm.Open() で発生するエラーが DB.Error に格納されるかどうかは調べていません。
あくまで「DB.Error には gorm.Open() のエラーが格納されない」と仮定した話です。


FunctionalOptionPattern による解決方法

FunctionalOptionPattern を利用すると、以下のような書き方になる。
MethodChaining の問題点は解消されているはず。

var user User
err = db.First(&user,
  Where("email = ?", "jon@calhoun.io"),
  Where("id = ?", 2),
)
if err != nil {
  panic(err)
}
fmt.Println(user)



MethodChaining としての解決方法

記事の内容としては、FunctionalOptionPattern を使おうっていう内容なんだけど、
記事中には以下の記載がある。

Note: One way to fix this would be to update methods like First and Find in GORM to return both the gorm.DB and an error when called, but I’m not 100% sure how this would affect the rest of the library. Instead we are going to explore an alternative approach that I prefer anyway

First(), Find() というメソッドが gorm.DB と error を返すようにする修正方法もある とのこと。

これはおそらく、以下のイメージだと思う。

db, _ := db.Open()
db, err := db.Where().Where().Find()
if err != nil {
	panic()
}



実装は以下。
MethodChainingの最後に Find() を実行させるので、
Find() だけが error を返せばいい。

package db

func Open() (*DB, error) {
	return DB{}, nil
}

type DB struct {
	err error
}

func (d *DB) Where() *DB {
	if xxx {
		d.err = errors.New("error!")
		return d
	}
	return d
}

func (d *DB) Find() (*DB, error) {
	return d, d.Err
}



クエリの構築であれば、
Build() で error を返すようにして、
Find() はチェインできるようにしてもいいと思う。

func (d *DB) Find() *DB {
	return d
}

func (d *DB) Build() (*DB, error) {
	return d, d.err
}



まあ、どちらにしろ、チェインの最後で error を返すようにしてあげればいいはず。

これによって、MethodChainingを利用するケースでも、
Error フィールドの問題点は解決することができる。


Error フィールドの問題点は本当に問題なのか?

元記事にある Error フィールドの問題点は、そもそも問題なのだろうか?

以下では、struct に Error フィールドを持たせることで、
エラーチェックの繰り返しを避けるテクニックが紹介されている。
https://blog.golang.org/errors-are-values

上記の記事でも紹介されているように、
標準パッケージの scan は Error フィールドを持ってるし、
https://github.com/golang/go/blob/f7ac70a56604033e2b1abc921d3f0f6afc85a7b3/src/bufio/scan.go#L38

エラーをチェックするためのメソッドも提供している。
https://github.com/golang/go/blob/f7ac70a56604033e2b1abc921d3f0f6afc85a7b3/src/bufio/scan.go#L90

エラーを戻り値で返すよりも、分かりやすいかどうか? という観点だと、
劣るかもしれないが、
少なくとも Error フィールドを持たせる実装がアンチパターンというわけではないはず。
なので、そもそも問題ではないのかもしれない。

元記事の筆者も より良い方法は FunctionalOptionPattern だよ と言っているだけで、
MethodChainingを否定しているわけではないと思う。
(わからんけど・・・


どちらを使うべきなのか?

正直、好みの問題なのかなーと思っている。

FunctionalOptionPattern はオプションの実装に interface を利用しているので、
ライブラリを利用する側が独自の実装を適用させることができる。
なので、不特定多数が利用するライブラリであれば、 FunctionalOptionPattern の方が適している気もする。

しかし、MethodChaining もメソッドの挙動を interface によって差し替えるような実装にすることで、
同じようなことが可能なので、
実装の差し替えは決定打にはならないなーというのが正直なところ。

FunctionalOptionPattern の方が実装が若干面倒な気がするけど、
MethodChaining に interface を導入した場合、
どちらも大して変わらないと思う。

個人的には、

MethodChaining = オブジェクトを組み立てるための実装パターン
FunctionalOptionPattern = オプションを指定するための実装パターン

という認識がある。

どちらを使っても目的は達成できるが、
そもそも用途が異なるかなーと思っているので、
とりあえず、上記の認識通りに使い分けていこうかなーと思っている。

そして、MethodChaining を利用する場合、
MethodChaining としての解決方法 を適用するかどうかも気になってくるが、
これもどっちでもいいと思う。

ただ、個人的には、戻り値で戻ってくる方が分かりやすいかなーと思うので、
適用可能であれば適用するかなーというところ。

まとめ

FunctionalOptionPattern or MethodChaining を悩むんだったら、
もっと他に悩むべきコードがありそうなので、
そっちを優先した方がいいと思う。