#概要
golangにおけるモデル(struct)の初期化関数とその利用方法について考えたことの書き起こし。
ベストプラクティスではなく、あくまで個人的な考えなので「ウチではこうしてる」的な意見をいただけると助かります。
#前提
この項目では当記事に用いる例の前提を書きます。
ユーザーによる何かしらの投稿機能を想定してください。
投稿に用いるモデルはPostモデルでそれぞれ以下のプロパティを持ちます。
type Post struct {
Message string //=> テキストメッセージ。ユーザーによる任意の値。
Key1 bool //=> ユーザーに入力させる属性その1。システム投稿の際はtrue。
Key2 bool //=> ユーザーに入力させる属性その2。システム投稿の際はtrue。
Default1 bool //=> とあるデフォルト値その1。初期化時はtrue。
Default2 bool //=> とあるデフォルト値その1。初期化時はfalse。
}
また、システムによる自動投稿も同じくPostモデルを用います。その際はKey1,Key2はtrueです。
#先に結論
デフォルト値を挿入する為のコンストラクタとして用いるのが管理がしやすくて良いと思いました。
またサービス上のモデルに対する細かいモデル?(いい言葉が思い浮かばないので募集...)ごとにコンストラクタを用意することでチームの共有や管理が楽になるんじゃないかと思います。
今回の例であれば、Postモデルに対してユーザーの投稿のコンストラクタ(NewPost)と、システム投稿のコンストラクタ(NewPostBySystem)を作成するといった感じです。
コードにすると以下の通り。
type Post struct {
Message string
Key1 bool
Key2 bool
Default1 bool
Default2 bool
}
// NewPost はPostの初期化関数
func NewPost() *Post {
p := new(Post)
p.Default1 = true
p.Default2 = false
return p
}
// NewPostBySystem はシステムによる自動投稿に用いるPostの初期化関数
func NewPostBySystem() *Post {
p := new(Post)
p.Key1 = true
p.Key2 = true
p.Default1 = true
p.Default2 = false
return p
}
var params = struct {
message string
key1 bool
key2 bool
}{"ユーザーによる投稿記事", false, true}
func main() {
// ユーザーの投稿作成
post1 := model.NewPost()
post1.Message = params.message
post1.Key1 = params.key1
post1.Key2 = params.key2
// システムの自動投稿作成
postBySystem := model.NewPostBySystem()
postBySystem.Message = "システムによる自動投稿"
fmt.Printf("%#v\n", post1)
// => &model.Post{Message:"ユーザーによる投稿記事", Key1:false, Key2:false, Default1:true, Default2:false}
fmt.Printf("%#v\n", postBySystem)
// => &model.Post{Message:"システムによる自動投稿", Key1:true, Key2:true, Default1:true, Default2:false}
}
#結論に到るまでのあれこれ
##引数を用いて初期化するパターン
ググると大体目につくのはこのパターン。
Javaのコンストラクタと同じ様に引数を渡して生成する方法。
func NewPost(message string, key1, key2 bool) *Post {
p := new(Post)
p.Message = message
p.Key1 = key1
p.Key2 = key2
p.Default1 = true
p.Default2 = false
return p
}
post := model.NewPost(
params.message,
params.key1,
params.key2,
)
悪いと思う点
- 引数の渡し方に注意を払うコスト
- モデルの変更に弱い
Golangは引数名を指定して引数を渡すことができないので、引数の順番に注意を払う必要が出てくる。
また、Postモデル自体に変更があった場合、初期化関数と呼び出し元の双方に修正が必要になる。
これなら直接モデルのstructを作った方がどこに何が入っているか読みやすい。(が、これも問題を抱えている。以下で説明。)
直接モデルのstructを作るパターン
post := model.Post{
Message: params.message
Key1: params.key1
Key2: params.key2
Default1: true
Default2: false
}
//あるいは以下
post := model.Post{}
post.Message = params.message
post.Key1 = params.key1
post.Key2 = params.key2
post.Default1 = true
post.Default2 = false
悪いと思う点
- 全てのプロパティを毎回書く冗長さ
- デフォルト値を想定している属性の書き漏れるリスク
今回はデフォルト値がある属性が二個なのでまだいいが、現場ではサービス上のフラグを管理するために多くの属性を用意しているケースが多い。それらを毎回書くのは手間であるし、漏れるリスクが大きいので良くない。
**「DBのデフォルト制約を設定して、Default1,2には値を入れずにDBに渡せば良くないか?」と思われるかもしれないが、Default1に対応するカラムにデフォルト制約「true(1)」を設定していてもfalseが入ってしまう。
なぜならGolangにはゼロ値(初期値)があり、宣言された時点でboolean型のDefault1,2には「false」が入る。
したがってDBでデフォルト制約を「true」にしていても「Default1 = false」**で保存されるので注意が必要。
post := model.Post{
Message: params.message,
Key1: params.key1,
Key2: params.key2,
}
// => model.Post{Message:"ユーザーによる投稿記事", Key1:false, Key2:true, Default1:false, Default2:false}
##デフォルト値の挿入として用いるパターン
冒頭の結論でも述べた通り私はこの方法が使い勝手がいいと思った。
func NewPost() *Post {
p := new(Post)
p.Default1 = true
p.Default2 = false
return p
}
良いと思った点
- モデルの変更に強い
- 見やすい
上記の様にデフォルト値だけコントラクタで挿入する様にしておけば、モデルの変更があってもデフォルト値だけ考慮して設定しなおせばいいので楽。
post1 := model.NewPost()
post1.Message = params.message
post1.Key1 = params.key1
post1.Key2 = params.key2
また、Golangはコンストラクタをいくつでも書けるので、細かいモデルケースにも応じることができるのではないかと考えた。
例えば今回であれば特定の処理中にシステムによる投稿をするのだが、システムの投稿はkey1,key2が常にtrueを想定していた。
普通に呼び出すと以下の様になる。
postBySystem := model.NewPost()
postBySystem.Message = "システムによる自動投稿"
postBySystem.Key1 = true
postBySystem.Key2 = true
この呼び出し方で問題として考えられるのは、この「システム投稿」のPostモデルの呼び出しが他の実装にも必要になったときである。
毎回Key1,Key2を設定するのは冗長であるし、他の実装者が「システム投稿」の仕様(Key1,Key2がtrue)を把握していなかった場合、漏れるリスクがある。
そこでKey1,Key2もデフォルト値と捉えて「システム投稿」用の投稿(Post)のコンストラクタを用意すると上記の問題を解決できる。
func NewPostBySystem() *Post {
p := new(Post)
p.Key1 = true
p.Key2 = true
p.Default1 = true
p.Default2 = false
return p
}
postBySystem := model.NewPostBySystem()
postBySystem.Message = "システムによる自動投稿"
// => model.Post{Message:"システムによる自動投稿", Key1:true, Key2:true, Default1:true, Default2:false}
結局「システム投稿」の投稿(Post)は「NewPostBySystem()」を利用するというルールをチームに伝達しないといけないが、諸処の属性の値(Key1,Key2)のルールを伝えるより楽だと思う。
#最後に
個人的に例に挙げた様なケースに直面した背景があったので色々考えました。
そもそもgolangはクラスもないのでコンストラクタもなく、コンストラクタ"風"に使っているだけっぽいのでJava等のコンストラクタ的な使い方に縛られずに柔軟に使えないか考えた結果たどり着いた一つの案が今回の記事です。
とはいっても個人的な案なので他の現場で「ウチではこうしてる」みたいな情報があれば共有していただけると助かります!