データ詰め替え戦略
このSpring Bootを使ったクリーンアーキテクチャの例は、データの詰め替え過剰にみえる。
これだけのモデルと詰め替えが必要なのだろうか?
『Get Your Hands Dirty on Clean Architecture 』にこのマッピング戦略(詰め替え戦略)が書かれている
No Mapping (レイヤ間でモデルを共有し、詰め替えをしない)
2-way Mapping (各レイヤで独自のモデルを持ち、レイヤを跨ぐ呼び出しは上位レイヤが詰め替えの責務を負う)
Full Mapping (各レイヤで独自のモデルを持ち、レイヤを跨ぐ呼び出しには専用のモデルを使う)
またこの戦略のどれを選ぶかの基準は『Balancing Coupling in Software Design』を合わせて検討すると良い。Balancing Couplingではモジュール間の統合強度として、次の2つを区別している。
モデル結合 (モジュール間でモデルを共有する。契約結合よりも強い結合)
契約結合 (モジュール間のやり取り専用のモデルを作る)
検討の軸
1. モデルを共有するか?
ありがちなレイヤードアーキテクチャにおいては、プレゼンテーション層やデータアクセス層でドメインモデルをそのまま使うか? という話である。
共有する
https://gyazo.com/0c10a9cb0786c754c7aadefac6e29f28
データアクセス層とドメインモデル
テーブル駆動設計の場合、テーブルとドメインモデルが1対1なので共有することはある。一般には、マスタメンテなど管理系機能であればマッチすることもあるが、テーブル変更=ドメインモデルの変更を意味するので、大胆なテーブル変更がしにくくなりがちなので、共有するかどうかは慎重に考慮する必要がある。
プレゼンテーション層とドメインモデル
プレゼンテーション層の出力がレンダリング済みのHTMLの場合、プレゼンテーション層がドメインモデルを直接参照することはよくある。
プレゼンテーション層の出力がJSONなどの場合は、ドメインモデルをそのままJSONシリアライズすると、機密情報漏洩に繋がることがあるので、一般には共有せず、プレゼンテーション層専用のモデルを作った方が良い。
共有しない
https://gyazo.com/46293b04089b954131b5d54f76796e2d
結合度を下げるためには「モデルを共有しない」を選択することになる。
モデルを共有しないのであれば、どこかで詰め替えが発生する。
データアクセス層のモデルはドメインモデルを使わず独自に作るとしても、真のデータ型定義はデータベースのテーブル定義であるので、同じ役割のものをプログラム上でのクラスなどで表現するのは冗長さがある。したがってテーブルに対応したクラスにマッピングするORマッパーではなく、SQLのResultSetを直接ドメインモデルにマッピングするようなデータアクセスライブラリを使う方がマッチするかもしれない。
2. モジュール間のデータのやり取りに専用のモデルを使うか?
結合度を真に弱めるには、モジュール間のデータのやり取り専用のモデルを作らないと、一方のモジュールの変更が他方へ波及することになる。つまりレイヤ間のやり取りに専用のDTOを使う。
https://gyazo.com/58e4c8f30712c06d3d51bdecffbd1c4b
こんなことが本当に必要なのかどうかの議論は昔からなされている。
ひがさんのブログに書かれているの2つ目の目的が、まさに専用のDTOを作ることに相当する。
この良し悪しを評価するには、Balancing Couplingの統合強度を考えると良い。このレイヤ間のやり取り専用のDTOを作らない限りは、統合強度は一段強いモデル結合になるが、Balancing Couplingではモジュール間の距離も同時に考慮に入れるべきだ、としており距離が十分近いならばバランスは取れているとする(すなわち同時に同じ人がメンテできるならば、統合強度が高くてもさほど辛みはない)。
なお、Balancing Couplingの結合強度と距離のバランスは次のように定義されている。https://gyazo.com/e3b7c72c3df274b6138592bd0f668f21
さらには、各層で独自のモデルを持つのであれば、その影響は以下のように限定される。
データアクセス層のモデルやプレゼンテーション層のモデルの変更は、ドメインモデルには影響を与えない。
ドメインモデルの変更は、データアクセス層のモデルやプレゼンテーション層のモデルに影響を与える。
これは実用上、一般には大きな問題がない選択といえるだろう。
3. 詰め替えをどちらがやるか?
レイヤードアーキテクチャで、レイヤーを跨ぐ呼び出しであれば、下位層が上位層のデータ構造に依存してはならないので、呼び出し側で詰め替えるのが正である。
code:usecase.ts
function usecase(...) {
const book: Book = ...;
const bookEntity = BookEntity.parse({
title: book.title,
price: book.price,
});
bookDao.insert(bookEntity);
}
ただしDIPを用いて、依存性をひっくり返した場合は、詰め替えの責務も逆になる。
code:usecase.ts
function usecase(...) {
const book: Book = ...;
bookDao.insert(book);
}
interface BookDao {
insert: (book: Book) => void;
}
code:dao.ts
class BookDaoImpl implements BookDao {
insert: book => {
BookEntity.parse({
title: book.title,
price: book.price,
});
}
}
詰め替えの実例
1. Clean Architecture w/ Full Mapping
過剰に見えるClean Architecture事例は、Full Mappingになっていることが多い。
https://gyazo.com/12f20d59c453287c10bcbbf9ab7cd07d
2. Ruby on Rails
アプリケーション全体としてNo Mapping戦略を標準とする代表例。
https://gyazo.com/0b0b387bf75012c1ce2eaed162d97ce9
3. Terasoluna
Terasolunaのガイドによれば、プレゼンテーション層のモデルとドメインモデルは別にする2-way Mappingであり、データアクセス層はドメインモデルを直接使うNo Mappingである。
これはテーブル駆動設計の代表的な詰め替え戦略で、十数年前はエンタープライズ領域で主流であった。
https://gyazo.com/0c931a08ac9198d8fa0090e5d45c648e