0
0

More than 3 years have passed since last update.

Golang で enum 。 enum に多様な属性や振る舞いを効率よく実装したい。

Last updated at Posted at 2021-07-13

自分だったらどうするかな、というのを書いてみました。2つ目の記事でも指摘されているような Switch 文はたしかに書きたくないので、配列のインデックスアクセスにする方法を採用しています。

個人的には enum は固有の情報を色々持てるのが嬉しいので、単に「分かりやすい文字列で識別できればいい」というわけではないんですよね。以下の例でも

  • 日本語での表現
  • 英語での表現
  • 次の季節が何か
  • 前の季節は何か
  • めぐる季節の順序判定

などの機能が enum の値に応じてちゃんと挙動を変えて動いてくれるようになっています。

使い方:

    hoge := enum.SeasonValueOf("ほげ")
    fmt.Printf("'ほげ' は '%s' の季節です。\n", hoge.Ja())

    fmt.Println("--------------------")
    w := enum.SeasonValueOf("WINTER")
    fmt.Printf("'WINTER' は日本語だと '%s' です。\n", w.Ja())
    fmt.Printf("'WINTER' は英語ならば '%s' です。\n", w.En())
    fmt.Printf("'WINTER' の次の季節は '%s' です。\n", w.Next().En()+"("+w.Next().Ja()+")")

    fmt.Println("--------------------")
    a := enum.SeasonValueOf("秋")
    fmt.Printf("'%s' は '%s' の次の季節でしょうか?: %t 。\n", w.Ja(), a.Ja(), w.IsNextOf(enum.Autumn))

出力結果:

$ go run sample.go
'ほげ''未定義' の季節です。
--------------------
'WINTER' は日本語だと '冬' です。
'WINTER' は英語ならば 'Winter' です。
'WINTER' の次の季節は 'Spring(春)' です。
--------------------
'冬''秋' の次の季節でしょうか?: true

実装:

package enum

import "strings"

type Season int

// enum の本体。実体は iota による int 値なのはよくあるそのまま。 [1]
const (
    Unknown Season = iota
    Spring
    Summer
    Autumn
    Winter
)

func (s Season) Ja() string                 { return s.get().Ja }        // 日本語表記文字列を返す
func (s Season) En() string                 { return s.get().En }        // 英語表記文字列を返す
func (s Season) Next() Season               { return s.get().Next }      // 次の季節を返す
func (s Season) Prev() Season               { return s.get().Prev }      // 前の季節を返す 
func (s Season) IsNextOf(other Season) bool { return s.Prev() == other } // 次の季節かどうかを判定
func (s Season) IsPrevOf(other Season) bool { return s.Next() == other } // 前の季節かどうかを判定
func (s Season) String() string             { return s.Ja() }            // ログ出力を考慮して String() も実装しておく

// 文字列表現などから enum を生成する関数。ウェブ開発などでビュー層からもらった文字列から作りたいときによく使う。
func SeasonValueOf(s string) Season { // hash lookup is more suitable than linear search?
    for _, season := range seasons {
        if strings.EqualFold(s, season.En) || s == season.Ja {
            return season.V
        }
    }
    return Unknown
}

// Season enum の多様な属性(情報)を表現する構造体の型
type attr struct {
    V    Season
    Ja   string
    En   string
    Next Season
    Prev Season
}

// enum の値に応じた属性(情報)を実際に定義している。 [2]
var seasons = [cnt]attr{
    {Unknown, "未定義", "unknown", Unknown, Unknown},
    {Spring, "春", "Spring", Summer, Winter},
    {Summer, "夏", "Summer", Autumn, Spring},
    {Autumn, "秋", "Autumn", Winter, Summer},
    {Winter, "冬", "Winter", Spring, Autumn},
}

// enum のバリエーション数。これを定数化し [2] と [3]  の両方に使っているのがみそだと思っている。
// enum が拡張されて [1] や [2] のバリエーション数が増えた場合、この cnt も変更され、 [3] の境界条件判定も自動で更新される。
const cnt = 5

// enum 値から属性情報をひっぱってくるメソッド。ここでありえない enum 値を制御できる。 [3]
func (s Season) get() attr {
    if s < 0 || s >= cnt {
        return seasons[0] // or panic("Illegal enum value.")
    }
    return seasons[s]
}

ポイント:

1.
enum の各値における多様な表現や機能を関数として簡単に呼び出せます。

enum.Summer.Ja()   //=> "夏"
enum.Winter.En()   //=> "Winter"
enum.Winter.Prev() //=> enum.Autumn

2.
構造体やその配列などの構造はすべて隠蔽されていおり、使う側からは触れません。

3.
enum に新しい情報や機能を持たせる横方向の拡張も enum 自体の値を増やす縦方向の拡張もどちらも容易に書けます。その際下記足りない項目があるとコンパイルエラーになって気付けます。

4.
元記事だと、存在しない整数値を無理やり作れてしまうのでその範囲確認をしていましたが、そこの上限値を定数化( cnt )し、さらに詳細な情報を持つ構造体の配列の Length 値にも流用しています。こうすることでのちのち enum を拡張して要素を増やした際に cnt を修正し忘れているとコンパイルエラーになり気付くことができます。

var seasons = [cnt]attr{
const cnt = 5
    if s < 0 || s >= cnt {

感想

順調に Golang を学び、楽しんでおります。

0
0
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
0
0