情報畑でつかまえてロゴ
本サイトは NTTテクノクロスが旬の IT をキーワードに
IT 部門が今知っておきたい最新テクノロジーに関する情報をお届けするサイトです

Grailsでトレイトを活用してみる

Groovy 2.3から導入されたトレイトを使っていますか?今回は、あまり気負わずに手軽にトレイトを使ってみる、という一例をご紹介します。

Grailsでトレイトを活用してみる

はじめに

こんにちは。NTTソフトウェアの中野です。Groovy 2.3から導入されたトレイトを使っていますか?今回は、あまり気負わずに手軽にトレイトを使ってみる、という一例をご紹介します。

トレイトとは

トレイトとはコンピュータープログラミングにおける概念で、構造的にオブジェクト指向プログラミングを行うための簡素な概念モデルとして使われるメソッド群の集まりである。

--- Wikipediaより https://ja.wikipedia.org/wiki/%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88


ざっくりいうと、実装を伴ったインタフェース、のようなものです。
これにより、Groovyでは実質的に多重継承が可能となります。

Java 8のデフォルトメソッドにも似ていますが、トレイト自身にプロパティを定義したり、利用側のインスタンス変数にもアクセスできるなど、より強力な機能になっています。

trait Named {
String name // トレイトはプロパティを持てる
}
trait GreetingAbility { String message
String greeting() { "$message, $name!" // 利用クラス(Person)に存在するプロパティを普通に利用できる } }
class Person implements Named, GreetingAbility { }
def person = new Person(name: 'Bob', message: 'Hello')
assert person instanceof Person
assert person instanceof Named assert person.name == 'Bob'
assert person instanceof GreetingAbility assert person.message == 'Hello' assert person.greeting() == 'Hello, Bob!'

利用例:Grails向けのtoStringメソッドの実装

以下のようなトレイトを定義しておくと、単にimplements ToStringAbilityするだけで、Grailsにおいてデバッグ用のプロパティをいい感じに出力するtoStringメソッドが使えるようになります。

/**
* オブジェクトの持つプロパティを出力する{code toString}メソッドを追加するトレイトです。
*/
trait ToStringAbility {
/** * {@code properties}を文字列化します。 * * {@code properties}の各値に対して、{@code null}以外の値を持つプロパティを出力する{code toString}メソッドを追加します。 * また、パスワード系の文字列はマスクします。 * バリデーションエラー等のerrorsプロパティがある場合は追加で出力します。 * * @return オブジェクトの文字列表現 */ @Override String toString() { return dumpProperties() + getErrorsIfExists() }
private String dumpProperties() { def props = this.properties.sort { it.key }.findAll { // 出力をシンプルにするため、値がnullのプロパティは除外する。 if (it.value == null) return false
// Grailsによる内部情報は除外する。 if (["constraintsMap", "constraints", "class", "errors"].contains(it.key)) return false
return true
}.collectEntries { name, value -> // パスワード系の文字列はマスクする。 [name, (name ==~ /.*([pP]ass(word)?|secret).*/) ? MessageUtil.maskedValue : value]
}.toString()
return "${this.class.simpleName}@${props}" }
private String getErrorsIfExists() { if (!hasProperty('errors')) return '' if (errors.errorCount == 0) return '' return " => ${renderErrors(this)}" }
private static List renderErrors(obj) { obj.errors.allErrors.collect { error -> MessageUtil.message(error) } } }
import grails.util.Holders
import org.grails.web.servlet.mvc.GrailsWebRequest
import org.springframework.context.NoSuchMessageException
import org.springframework.validation.ObjectError
/** * Grailsのi18n機構によるメッセージ解決をどこからでも使えるようにするためのユーティリティクラスです。 */ class MessageUtil { /** エラーメッセージを返します。*/ static String message(ObjectError error) { try { def messageSource = Holders.applicationContext.messageSource return messageSource.getMessage(error, locale) } catch (NoSuchMessageException e) { // キーに対応するメッセージが存在しない場合は例外がスローされる。 // エラーのデフォルトメッセージ文字列を返す。 return error.defaultMessage } }
/** コードに対応するメッセージを返します。*/ static String message(String code, List args, String defaultMessage = null) { try { def messageSource = Holders.applicationContext.messageSource return messageSource.getMessage(code, args as Object[], locale) } catch (NoSuchMessageException e) { // キーに対応するメッセージが存在しない場合は例外がスローされる。 // ここでは、g:messageと同様にキーそのものを返す。 // ただし、デフォルトメッセージが指定されている場合は、指定された文字列を返す。 return defaultMessage ?: code } }
/** エンドユーザから指定されたロケールを返します。langパラメータにも対応しています。*/ private static Locale getLocale() { GrailsWebRequest.lookup()?.locale ?: Locale.default }
/** マスク文字列を返します。*/ static getMaskedValue() { message("masked.value", []) // messages.propertiesで「masked.value=****」を定義しておくこと } }
// 利用例
class Book implements ToStringAbility {
String title
String author
String secretInfo
String option
}
println new Book(title: "プログラミングGROOVY", author: "中野、他", secretInfo: "32ページ目が破れている").toString() //=> Book@[author:中野、他, secretInfo:****, title:プログラミングGROOVY

このToStringAbilityトレイトでは、

  • 値がnullのプロパティは除外する
  • パスワード系の値をマスクする(****で置換する)
  • Grails/Groovyによる内部プロパティは除外する
  • バリデーションエラーがある場合に付加情報として出力する

などをサポートしています。なお、今回のサンプルコードでは、(判明している範囲で)以下の制限事項があります。

  • プロパティの循環参照を考慮していないので双方向関連を持つドメインクラスに適用するとStackOverFlowErrorが発生する

他の実装手段との比較

わざわざトレイトを使わなくとも、既存の手段で似たようなことは実現できます。それらの手段との違いを簡単にみてみましょう。

AST変換

Goovyが標準提供しているAST変換の@groovy.transform.ToStringでも、toStringメソッドの自動生成が可能です。

@groovy.transform.ToString(includeNames=true, ignoreNulls=true, excludes=['secretInfo'])
class Book {
String title
String author
String secretInfo
String option
}
println new Book(title: "プログラミングGROOVY", author: "中野、他", secretInfo: "32ページ目が破れている").toString() //=> Book(title:プログラミングGROOVY, author:中野、他)

これ自体は非常にお手軽で、私もよく使います。ただし、出力に対して想定外のカスタマイズはできません。既存の仕様を超えたカスタマイズは不可能ですし、独自AST変換として再実装するのはそれなりの手数が必要となり、実装コストやメンテナンスコスト的にはほとんどペイしません。その点、トレイトであれば実装自体もごく直感的ですし、プロジェクトごとの都合に合わせたカスタマイズも容易です。

とはいえ、groovy.transform.ToStringは小回りのきくオプションを色々持っていて、多くの場面ではオプションを調整するぐらいでそれほど困らないと思います。

今回の例のトレイトでは、バリデーションエラー情報の出力機能がgroovy.transform.ToStringにはないプラスアルファな部分になります。

ユーティリティクラス

トレイトを使わずに、単にユーティリティクラスに同等の処理を実装して、toStringメソッドの実装としてユーティリティメソッドを呼び出す方法もあります。この場合ユーティリティメソッドを呼ぶだけの、同じ内容の数行のtoString実装が利用クラスごとに必要になり、DRYさに欠けます。

class ToStringUtil {
static String toString(Object obj) {
// ...コードの雰囲気がわかれば良いので、細かい処理は省略する...
return "${obj.class.simpleName}@XXXX"
}
}
// 利用例 class Book { String title String author String secretInfo String option
@Override String toString() { ToStringUtil.toString(this) } }
println new Book(title: "プログラミングGROOVY", author: "中野、他", secretInfo: "32ページ目が破れている").toString() //=> Book@XXXX

共通スーパクラス

いちいち実装するのが面倒ということであれば、共通のスーパクラスを用意してしまう、という手も一応あります。しかし、この方法は現実的ではありません。is-a関係ではないクラス間で継承を使うというアンチパターン構造になってしまいますし、他に継承が必要なクラスがある場合には使えません。

class MySuperClass {
@Override
String toString() {
// ...コードの雰囲気がわかれば良いので、細かい処理は省略する...
return "${obj.class.simpleName}@XXXX"
}
}
// 利用例 class Book extends MySuperClass { String title String author String secretInfo String option }
println new Book(title: "プログラミングGROOVY", author: "中野、他", secretInfo: "32ページ目が破れている").toString() //=> Book@XXXX

[おまけ]GDKメソッドのObject#dump()

ユースケースは異なりますが、単にデバッグやロギングでプロパティ情報を確認したいだけであれば、GDKメソッドのObject#dump()が便利です。

class Book {
String title
String author
String secretInfo
String option
}
println new Book(title: "プログラミングGROOVY", author: "中野、他", secretInfo: "32ページ目が破れている").dump() //=> <Book@374470fc title=プログラミングGROOVY author=中野、他 secretInfo=32ページ目が破れている option=null>

おわりに

ここで紹介したように、共通化できる処理がある場合に、トレイトによる実装も候補のひとつとして視野に入れてみるのはいかがでしょうか。

あと、サンプルコードにしれっと入れておきましたがMessageUtilの実装はけっこう便利なので、Grailsアプリの開発時にご参考ください(なお、品質等につきましては保証いたしかねます)。

連載シリーズ
中野靖治のGroovy活用術
著者プロフィール
中野 靖治
中野 靖治
JVM上で動作する動的型付け言語であるGroovyと、Groovyで記述するWebアプリケーションフレームワークのGrailsを社内外へ推進するために日々奮闘している。 Groovyスクリプトの起動時間を短縮するGroovyServや、GroovyスクリプトでのExcel操作を劇的に楽にするGExcelAPIなどのOSSを業務/プライベートで開発、 一般に公開。Groovy/Grails/GradleなどのOSSへのバグフィックスや機能パッチの提供などにも積極的に貢献している。 また、国内外(JJUG CCC、Java Day Tokyo, Gr8conf EUなど)での講演や書籍執筆などでも活動中。 著書に『プログラミングGroovy』(技術評論社/共著)がある。自他共に認めるビール党。