読者です 読者をやめる 読者になる 読者になる

uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかFregeとかJavaとか -

enumに継承を! traitとenumの妙な関係、もしくはGrailsのドメインクラスの選択フィールドを国際化表示するのにtraitが便利

grails i18n trait enum

Grailsドメインクラスにおいて、いくつかの候補の数値のいずれか、というフィールドを作成し、scaffoldで生成した画面からCRUD操作したいとします。

簡単なのは、こうですね。

class Domain {
  Integer something

  static constraints = {
     something inList:[1,2,3]
  }
}

しかし数値フィールドに量としてではなく個々の値にそれぞれの意味がある場合、たとえば、腹の状態を表すフィールドの値として、

  • 1: はらぺこ
  • 2:まんぷく
  • 3:こばらがへった

を表現するようなものだったとします。このとき、すくなくとも、Scaffoldの画面上では数値ではなく意味のわかる文字列で表示したり選択入力させたいわけです。しかしデータそのものを文字列にするのもDB上の表現をそうしたくないのでいやだとします*1
ということでenumの出番です。

class Domain {
  enum OnakaStatus {
     STARVATION(1),
     SATICEFIED(2),
     SOSO(3)
     private final int id;
     private OnakaStatus(int id) {
         this.id = id;
     }
     public int getId() {
        return id;
      }
  }
  OnakaStatus onakaStatus
  static constraints = {
//     onakaStatus inList:[OnakaStatus.STARVATION,OnakaStatus.SATICEFIED,OnakaStatus.SOSO]
// よく考えるとこんなconstraintはいらない。enumなんだから。
  }
}

"STARVATION","SATICEFIED","SOSO"を選択肢とするセレクト/オプションタグをscaffoldが生成し、FORMパラメータとしてはその文字列、DB中には対応する数値(1,2,3)が保存されます。ナイス! GORMナイス!
でもこれだと表示が日本語にならないし。"STERVATION"の代りに"はらぺこ"というenumの識別子を使えば表示が日本語にはなるが、FORMパラメータとしてその日本語文字列が使われるのは好きくない。ではこうしてみてはどうか。

class Domain {
  enum OnakaStatus {

     STARVATION(1, "はらぺこ"),
     SATICEFIED(2,"まんぷく"),
     SOSO(3,"こばらがへった")
     private final int id;
     private final String value
     private OnakaStatus(int id, String value) {
         this.id = id;
         this.value = value;
     }
     public int getId() {
        return id;
      }
     String toString() {
         return value
     }
  }
  OnakaStatus onakaStatus
}

一応これで識別子は英字のまま表示を日本語にできます。しかしながら、国際化対応ができません。
ぐぬぬ
そういう場合は、enumにorg.springframework.context.MessageSourceResolvableを実装させて、以下のメソッドを定義し、

   enum OnakaStatus implements org.springframework.context.MessageSourceResolvable {
     :
    public Object[] getArguments() { [] as Object[] }
    public String[] getCodes() { [ name() ] }
    public String getDefaultMessage() { "?-" + name() }

   }

この上でi18nメッセージをenumの要素をキーとして用意します。たとえば、grails-app/i18n/なんとか.propertiesに、

STARVATION=はらぺこ
SATICEFIED=まんぷく
SOSO=こばらが減った

でも、enumごとにメソッド追加するの〜! えー冗長。やだーやだー。
ぐぬぬぬ。
共通するメソッドを親クラスに切り出したいところですが、enumはクラスからの継承(extends)はJava/Groovyの言語仕様上許されておりません。interfaceからのimplementsなら可能なのだが。はっ!!
ということでtraitを使えばいいのです。

trait I18nEnum implements org.springframework.context.MessageSourceResolvable {
    public Object[] getArguments() { [] as Object[] }
    public String[] getCodes() { [ name() ] }
    public String getDefaultMessage() { "?-" + name() }
}

こういうtraitを用意して、enumはこうして

    enum OnakaStatus implements I18nEnum {
        STARVATION(1),
        SATICEFIED(2),
        SOSO(3)
        private int id;
        private OnakaStatus(int id) {
            this.id = id;
        }
        public int getId() {
            return id;
        }
    }

あらすっきり*2

enum継承できないのでメソッドを共有させることが本来はできないわけですが、implementsはでき、試したところtraitからもできた。なのでenumメソッドを共有化できました。めでたい。Groovy便利。

Java8のデフォルトメソッドでどうかは不明。

*1:文字列にした上で、フィールドはtransientにしておいて、beforeSaveとかのタイミングで値を適切にさしかえるという手はある。でも国際化の問題はある。

*2:もっとすっきりさせようと、idとgetIdをtraitに移動させてみたり、@InheritConstructorなども試してみたがうまくいかなかった。