#はじめに
関心の分離を意識した名前設計で巨大クラスを爆殺するにてシステムを分割する手法について勉強させて頂きました。
書かれている内容を基に自分なりにシステムの設計を考えていたところ疑問や気付きがあったので投稿してみます。
間違っていることを記載していたらご指摘願います。
#境界づけられたコンテキストでシステムを分割
関心事に相応しい命名をするの項で以下の図がありました。
自分的にはこれは、同じ商品であっても状況(コンテキスト)によって関心事が変わるので、境界づけられたコンテキストで分けるのかなと思っています。(DDD的な)
これを単純にサブドメイン -> システムに変換すると
とかCQRS+ESだと
みたいにマイクロサービス的にシステムを分割しないといけないのかなぁと思っていました。
#集約単位で分割
システムで分けるとしたら自分は在庫品ID, 予約品ID, 注文品ID, 発送品IDというのを用意して、外部キーでそれぞれのIDを持つような設計しか考えていなかったんですけど
同じく関心事に相応しい命名をするに書かれている
(※在庫品、予約品、注文品、発送品の一意性は、同じユニークIDを使うことで解決します。)
で別の視点に気付かせてもらいました。
商品IDなんかを用意してそれぞれの集約のユニークIDにすれば良いのかと。
その考えを元にクラス図を書いてみると
[](
@startuml
package 共通 {
class 商品ID <> {
-id:GUID
}
class 商品名 <> {
-name:string
}
}
package 予約 {
class 予約品 <<集約>> {
-ID:商品ID
-発売日:発売日[VO省略]
-予約者:予約者[VO省略]
}
interface 予約リポジトリ {
+save(予約品): void
+get(商品ID): 予約品
}
}
package 注文 {
class 注文品 <<集約>> {
-ID:商品ID
-注文日:注文日
-注文者:注文者[VO省略]
-支払い方法:支払い方法[VO省略]
}
interface 注文リポジトリ {
+save(注文品): void
+get(商品ID): 注文品
}
class 注文日 <> #red {
-_注文日:DateTime
}
}
package 在庫 {
class 在庫品 <<集約>> {
-ID:商品ID
-出品者:出品者[VO省略]
}
interface 在庫リポジトリ {
+save(在庫品): void
+get(商品ID): 在庫品
}
}
package 発送 {
class 発送品 {
-ID:商品ID
-注文日:注文日
-受取人:受取人[VO省略]
-発送先:発送先[VO省略]
}
interface 発送リポジトリ {
+save(発送品): void
+get(商品ID): 発送品
}
}
商品ID --* 予約品
商品ID --* 注文品
商品ID --* 在庫品
商品ID --* 発送品
注文 -[hidden]right- 予約
注文 -[hidden]down- 在庫
在庫 -[hidden]right- 発送
@enduml
)
こんな感じになるのかと。
ここでひとつ疑問に思ったのが、例えば注文サブドメインに属していて注文品が管理している注文日という業務知識があって、
発送サブドメインでも使いたい場合はどうするのだろう、と。
注文して発送する、というユースケースを考えた時に
interface I注文する {
void 注文する(注文品 _注文品);
}
interface I発送する {
void 発送する(注文品 _注文品, 発送品 _発送品);
void 発送する(注文日 _注文日, 発送品 _発送品);
}
のように注文品を丸々渡したり注文日だけを抽出して渡したりするの?と思いました。
もしくは
注文リポジトリ.Save(注文品);
発送リポジトリ.Get(注文品.ID);
のように一度保存領域に保存してから発送品を取得するとか。
前者はインターフェースの引数増えたらどうすんの?とか
後者は処理記述が前後したらどうなるの?とか(手続き型になってる)
考えてもやもやしてました。
あと商品名をどこに置こうかな?とか。
#DCI的な考え方に変更してみる
そんなときにDCIアーキテクチャという考え方を見つけることが出来ました。
DCIとは、については自分では語れません。(すいません。)
ただDCIはdata context interactionの略だそうです。
自分的な理解でDCI的に書き直してみると
[](
@startuml
package 共通 {
class 商品ID <> {
-id:GUID
}
class 商品名 <> {
-name:string
}
class 注文日 <> #red {
-_注文日:DateTime
}
class 商品 <<集約>> {
-ID:商品ID
-商品名:商品名
-発売日:発売日[VO省略]
-予約者:予約者[VO省略]
-注文日:注文日
-注文者:注文者[VO省略]
-支払い方法:支払い方法[VO省略]
-出品者:出品者[VO省略]
-受取人:受取人[VO省略]
-発送先:発送先[VO省略]
}
interface 商品リポジトリ {
+get(商品ID): 予約品
}
}
package 予約 {
interface 予約ロール {
+ID:商品ID
+発売日:発売日[VO省略]
+予約者:予約者[VO省略]
+予約する():void
}
}
package 注文 {
interface 注文ロール {
+ID:商品ID
+注文日:注文日
+注文者:注文者[VO省略]
+支払い方法:支払い方法[VO省略]
+注文する():void
}
}
package 在庫 {
interface 在庫ロール {
+ID:商品ID
+出品者:出品者[VO省略]
+在庫追加する():void
+出品する():void
}
}
package 発送 {
interface 発送ロール {
+ID:商品ID
+注文日:注文日
+受取人:受取人[VO省略]
+発送先:発送先[VO省略]
+発送する():void
}
}
商品ID --* 商品
注文日 --* 商品
商品名 --* 商品
商品 --|> 予約ロール
商品 --|> 注文ロール
商品 --|> 在庫ロール
商品 --|> 発送ロール
package アプリ層 {
interface I予約するユースケース {
+予約する(I予約ロール _商品);
}
interface I注文するユースケース {
+注文する(I注文ロール _商品);
}
interface I出品するユースケース {
+出品する(I出品ロール _商品);
}
interface I発送するユースケース {
+発送する(I発送ロール _商品);
}
}
note left of 予約 : ロールのメソッドは\nインターフェース実装しておく
@enduml
)
となります。
結局商品はというただのデータ置き場が出来上がり、各状況(コンテキスト)での振る舞いはそれぞれの役割(ロール)に記述します。
商品は各ロールを継承しているのでユースケースを実行する際にロールにキャストして使用します。
例えば
var 商品 = 商品リポジトリ.Get(new 商品ID());
I注文するユースケース 注文 = new 注文するユースケース();
注文.注文する(商品);
I発送するユースケース 発送 = new 発送するユースケース();
発送.発送する(商品);
のように同一インスタンスを使い回せます。
これであれば注文日がI注文するユースケースで変更されていても、I発送するユースケースでは変更済みの注文日を使用できます。
商品名の置き場所も商品に決定出来ましたが商品名を変更しようとなると商品諸情報変更ロールなるロールを用意しないといけないのかな?と思っています。
あとロールが増えれば増えるほど商品のプロパティが増えそうで、これで合っているのかどうか自信がないです。
#まとめ?
だらだらと書いてしまいましたがこの投稿では
- 関心を分離して別集約にしてもユニークIDを使い回せば同一視できる
- ほぼほぼデータが同じで少しだけ振る舞いが違う場合はDCIが有効かも
- DDDとDCIは混ぜると危険?
という点について記載したつもりです。
DCIについて正しく理解している自信がないので間違い等ありましたらご指摘願います。