LoginSignup
22
11

More than 3 years have passed since last update.

Golangのコンストラクタとその利用方法についての考察

Last updated at Posted at 2019-11-21

概要

golangにおけるモデル(struct)の初期化関数とその利用方法について考えたことの書き起こし。
ベストプラクティスではなく、あくまで個人的な考えなので「ウチではこうしてる」的な意見をいただけると助かります。

前提

この項目では当記事に用いる例の前提を書きます。

ユーザーによる何かしらの投稿機能を想定してください。
投稿に用いるモデルはPostモデルでそれぞれ以下のプロパティを持ちます。

model
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)を作成するといった感じです。

コードにすると以下の通り。

model.go
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
}
main.go
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
}
invoke
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」で保存されるので注意が必要。

Defaultの値を入れないとゼロ値(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
}

良いと思った点

  • モデルの変更に強い
  • 見やすい

上記の様にデフォルト値だけコントラクタで挿入する様にしておけば、モデルの変更があってもデフォルト値だけ考慮して設定しなおせばいいので楽。

invoke
post1 := model.NewPost()
post1.Message = params.message
post1.Key1 = params.key1
post1.Key2 = params.key2

また、Golangはコンストラクタをいくつでも書けるので、細かいモデルケースにも応じることができるのではないかと考えた。
例えば今回であれば特定の処理中にシステムによる投稿をするのだが、システムの投稿はkey1,key2が常にtrueを想定していた。

普通に呼び出すと以下の様になる。

invoke
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
}
invoke
postBySystem := model.NewPostBySystem()
postBySystem.Message = "システムによる自動投稿"
// => model.Post{Message:"システムによる自動投稿", Key1:true, Key2:true, Default1:true, Default2:false}

結局「システム投稿」の投稿(Post)は「NewPostBySystem()」を利用するというルールをチームに伝達しないといけないが、諸処の属性の値(Key1,Key2)のルールを伝えるより楽だと思う。

最後に

個人的に例に挙げた様なケースに直面した背景があったので色々考えました。
そもそもgolangはクラスもないのでコンストラクタもなく、コンストラクタ"風"に使っているだけっぽいのでJava等のコンストラクタ的な使い方に縛られずに柔軟に使えないか考えた結果たどり着いた一つの案が今回の記事です。

とはいっても個人的な案なので他の現場で「ウチではこうしてる」みたいな情報があれば共有していただけると助かります!

22
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
11