Javaで、CRUD操作を受け付けるWebAPIを作るとします。永続化はRDBです。フレームワークは特に限定しません。JavaEE系フレームワークかもしれないしPlay!かもしれないし、SOAPサービスにすることにしてAxis2かもしれません。
SOAPでXMLシリアライズするにしろJSONシリアライズするにしろ、デシリアライズして入力データを受け取るデータクラスは作りますよね。
public class Item {
public Integer id;
public String name;
public String model;
public Integer price;
}
と、こんなデータクラスを作ってみました。プロパティgetter/setterのことは今は考えないでおきましょう。作りません。
さて、Item作成メソッドを作るとして、このあとどういう流れでこのデータをDBに突っ込みましょうね。どんなフレームワークを選択したにしろO/Rマッパーやそれに類するものはあるとします。
- Itemクラスを、O/Rマッパーのエンティティクラスも兼ねるように作りますか?
-- それはまずいですね。JSONデシリアライズとO/Rマッピングは全然別の関心対象です。というか層が2段階ぐらい離れています。これがいわゆる 密結合 。モジュールや層同士で共有すべき知識(ここでは同じクラスを使う)が多ければ多いほど、修正を加えたときの影響範囲が測れなくなります。
第一考えてみてください。RDBの都合でカラム名を変更したとき、Itemクラスのフィールド名も影響を受けることになります。そしたらJSONのキー名も自動的に変更ってことに? 内部の設計変更がAPIまで変更してしまうなんて完全に間違っていますよね(アノテーションでカバーできるでしょって対策の存在は本質ではないのでここでは目をつぶります)。 - HTTP/JSON入出力層、ビジネスロジック層、永続化層で別々にItem格納クラスを用意し、 層をまたぐたびに“積み替え” を行わせますか?
-- これは良識的ではあります。しかしうんざりするプログラミングが待っています。“積み替え”のコードはまったくもって機械的に、ほとんど同じフィールドを持ったクラス同士でのディープコピー。コードが冗長なら動作も冗長。大量のデータを読み出す際にはパフォーマンスが、特にメモリ効率が気にもなってきます。
フレームワーク非依存なプログラミングにしたいのです
上記の二つの選択肢、前者がまずいのは入出力層と永続化層が密結合しているという点ではありません。本体であるビジネスロジック層が入出力層と永続化層に密結合している、それがまずいと言うべきです。
こう考えましょう、ソフトウェア本体はフレームワークに関係なく、フレームワークに何を使っているのかを知ることなく書かれるべきだと。
フレームワークはいつか開発が止まって、その後に重大な脆弱性が発覚するかもしれません。もしくは他言語への移植を要求されるかもしれません。ASP.NET MVCで動かす必要があるんだけどJavaからC#ならだいたい逐語訳で書き換えられるよね、なんて要求が。
そしてなにより、本当にあるかわからない移植話より、テスタビリティ・リーダビリティです。疎結合なら各処理の独立性、参照透過性は確保しやすく理解・把握が容易に、もちろんテストコードを書くのも容易になります。逆だ。疎結合にしておかないとすべてが困難というかほぼ不可能になります。
getterのみのinterfaceを抽出しよう
長い前振りで結局どうしろと言いたかったか。前者はダメ、後者は非効率的、では第三の道が? それがインターフェースの抽出です。
Item追加という仕事においてビジネスロジック層の立場をもう一度明確にしてみます。
- 入出力層からは、追加するItemのデータを受け取れればいい。 それはどんなクラスであれ、name, model, priceを読み取れれば必要十分 。
- 永続化層は、渡したデータを保存して付番されたIDを教えてくれればいい。 そのデータは、name, model, priceを読み取れるものだ 。
ごく簡単な話で、Itemはクラスではなくinterfaceであるべきだったのです。
public interface Item {
String getName();
String getModel();
Integer getPrice();
}
class Controller {
/** ビジネスロジック層のItem新規作成メソッド */
void createItem(Item newItemInfo) {
...
// ビジネスロジック層のどこかで永続化層に保存を依頼する
Integer id = db.items.add(newItemInfo);
...
}
}
HTTP/JSON入出力層では、JSONデシリアライズ用クラスを独自に書きますが、ビジネスロジック層に渡すとき積み替えをする必要はありません。デシリアライズ用クラスがItemを実装していればいいのです。
public @Lombok.Data class CreateItemInput implements Item {
public String name;
public String model;
public Integer price;
}
getter/setterは自分で書かずにLombokに自動実装させてしまいましたが、ここでまさにフィールドをプロパティ化したことが活きてきました。
- ビジネスロジック層は受け取るオブジェクトの型もinterfaceとして表現したい。
- データクラスをプロパティ化しておけばinterfaceを実装できる。
こういう流れです。
さて永続化層側。永続化層としてはデータをインターフェースで受け取ってもそのままではO/Rマッピングエンティティ扱いできない? そんなことはあるでしょう。でも積み替えまでする必要はありません。エンティティクラスにコンストラクタ引数をちょっと追加して、ラッパークラスの形にするだけです。
@Entity(name = "items")
class ItemEntity {
private Item item;
public ItemEntity(Item item) { this.item = item; }
@Lombok.Getter @Lombok.Setter
public Integer id;
public String getName() { return item.getName(); }
public String getModel() { return item.getModel(); }
public Integer getPrice() { return item.getPrice(); }
}
ラッパーインスタンス作成が一回だけ発生するけど積み替えに比べれば合理的な範囲です。
とは言え、ラッピングが必要ということは永続化層はモデルをインターフェース化する恩恵には与れないのでしょうか? いえ、そんなことはありません。逆の流れ、DBから取り出したデータを入出力層まで返していくときには今度は永続化層側が恩恵を受けますね。ビジネスロジック層が求める戻り値型がinterfaceで表現されていて、SELECT用のエンティティクラスがそれを実装していればいいという話です。
ここでもし知らなければワイルドカード総称型のことも覚えておくと役に立ちます。
上記のデータ返しを書こうとするとおそらくIterable<ItemEntity>
をIterable<Item>
に代入できないという問題に突き当たるはずです。ItemEntity
implements Item
なのに。
こういう場合、受け取る側の型をIterable<Item>
ではなくIterable<? extends Item>
とすると受け取れます。これが共変型のワイルドカード総称型。
List
でなくIterable
なのも重要な点ですが話がそれすぎるので別の機会に。
まとめ
データクラスではなくデータインターフェースを定義するようにすれば密結合させず、かつ積み替えも発生させずに層間結合を書けますよ!
getter/setterも、いらない子じゃなかった!
どうせLombok使うから見えない子だけど!