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

C Sharpens you up

http://qiita.com/yuba に移しつつあります

EBeanでもユニットテストの導入はスマートです。そう、Lombokならね。

Play! 2.x Play framework 2.x Java and 1.x Advent Calendar 2013

Play framework 2.x Java and 1.x Advent Calendar 2013*1の6日目です。

お題は、Lombokを使ってPlay2 Javaユニットテストを書きやすくできますよという話。

ケース

Play2 Java使っている方なら当然、永続化はEBeanですよね。そうでしょうそうでしょう。では、例としてこんな感じのテーブル

CREATE TABLE items
(
  id serial NOT NULL PRIMARY KEY,
  name text NOT NULL
);

を、こういうエンティティクラス

@Entity @Table(name="items")
public class Item extends Model {
    @Id @NotNull
    public Integer id;
    @NotNull
    public String name;
}

O/Rマッピングしたとしましょう。
そして、このエンティティクラスを使って要求を処理するビジネスロジックを書きます。ビジネスロジックにはユニットテストも書きましょう。

さて、ユニットテストはDBの現在状態に依存したりDBに変更を加えたりしてほしくありません。
ではテストのときだけインメモリのH2を使おうかというと、ここで定義したテーブルには出てこないのですがスキーマの他の部分ではPostgreSQLに依存した設計をしてしまっていてH2では代わりにならない状況です。困りました。

ここで我々はこう考えます。
「そうだ、エンティティクラスというフレームワーク独自のクラスにビジネスロジックが依存してしまっているのがそもそもおかしいんだ。ビジネスロジック層はEBeanのエンティティクラスそのものではなくそこから抽出したインターフェースを扱うようにしないと」

正論ですね。しかし手を動かそうとしたらすぐ固まります。

フィールドはインターフェースに抽出できないじゃないの…」

ここ泣くところです。せっかくEBeanはJava Beansと決別してあのgetter/setterのいらないプログラミングを提供してくれました。なのにインターフェースの抽出をしようとするとJavaは冷酷にもgetter/setterを使ったアクセスしかないよねと迫ってくるのです。

醜く変貌したエンティティクラスを一応書きました*2。ご覧ください。

@Entity @Table(name="items")
public class Item extends Model implements IItem {
    @Id @NotNull
    protected Integer id;
    @NotNull
    protected String name;

    @Override
    public Integer getId() {
        return id;
    }

    @Override
    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }
}
そこでLombok

しかしです。絶望だらけのこの世にもLombokがある。
Lombokをご存じない方はこの記事

とかこのプレゼン資料

を見てもらえばわかりますが、Javaによくある冗長な記述をコンパイラに介入する黒魔術でガンガン省略させてくれるアノテーションライブラリです。

これを使えば、getter/setterはコードに書かなくてもいいということです。これってつまり、エンティティクラスを単純に保ったままインターフェースが抽出できるってことじゃない? いいじゃん!

コンパイラに介入するという点ではEBeanもそうなので*3、てっきりEBeanとLombokは共存できないものと思い込んでいました。最近まで。ところがやってみたらあっさり共存できてしまった次第。もう絶望する必要なんて、ない。

導入方法

Play 2.0/2.1ならBuild.scala、2.2ならbuild.sbtで依存にLombokを追加します。

  "org.projectlombok" % "lombok" % "1.12.2",

実はこれだけでビルドファイル的には準備完了なのですがIDEが理解してくれないといろいろ赤波線とかあれですから、プロジェクトファイルを作り直してIDEにもLombokライブラリを参照してもらいます。

【プロンプト】 play idea

ここではIntelliJ IDEAを想定していますが、IntelliJ IDEAの場合さらに、プラグインとしてLombokをリポジトリから探してインストールします。

改善後

あの醜かったエンティティクラスはこう書き換えられます。

@Entity @Table(name="items")
@Data
public class Item extends Model implements IItem {
    @Id @NotNull
    public Integer id;
    @NotNull
    public String name;
}

最初のクラスに比べると@Dataの記述が加わっただけですね。シンプル!
EBeanの崇高な理想は守られました!

その他

インターフェースの抽出をした場合、ファクトリメソッドの書き方に困る場合があります。

public List<Item> fetchAll();

なんてメソッドをインターフェース抽出にともなって

public List<IItem> fetchAll();

に書き換えようとすると、その内部処理で詰まります。なぜならList<Item>List<IItem>にキャストできないから。

こういうときは、

で書いたように

public List<? extends IItem> fetchAll();

というシグネチャにすると幸せになれます*4

明日は

@aimluck_iwasakiさんがサブプロジェクトや運用の話題をとのことです。

*1:Advent Calendarとはクリスマスまでのカウントダウン日めくりのことで、それになぞらえて12/1から12/25まで日替わりで参加者がブログ記事を寄稿するイベントです。

*2:IItemっていう命名センスがそれC#だよねってのはちょっと目をつぶっててください。

*3:Lombokはコンパイルの途中(構文木ができたところで構文木にgetter/setterを差し込む)、EBeanはコンパイル後(.classファイルができてから.classファイルにgetter/setterを差し込む)なのでやっていること同じとは言え過程が全然違うんですけどね。

*4:IBMの出してる解説記事ではそういうのやめろよと言われちゃってますが。