こちらのイベントに参加の中で、「DDDモデリングハンズオン」というコンテンツがあり、モデリングしながらドライバーの方がライブでコードを書いていただくというとても良いコンテンツがあったので参加してきました。
そこでは携帯の月額プランをお題にモデリングをされていました。
そこで参加した内容が時間内に終わらなかったこともあり、自分でも手を動かしてみたくなったので自分の普段のやり方でモデリングしてみました。
ドメインモデリングをどういう風にするか、というのをブログに書いたこともなかったので、せっかくなので紹介させてくださいm(_ _)m
ユースケース図
モデリングする際は、僕は必ずユースケース図から始めます。
モデリングというのは何か問題を解決するために現実世界の一部を抽象化することなので、どのように使用するのかを定めなければモデリングの目的がわからなくなってしまいます。
ユースケース図を書くことで、モデリング参加者の間でモデルの用途の認識が揃います。また、その会でモデリングするスコープを限定する効果もあります。体外に置いてシステム化したいものは多くあるので、スコープを切らないと際限なく広がってしまい話が発散してしまいがちなのです。
ということで、今回はこんな感じに切ってみました。シンプルですね。
でもシンプルでもあるだけでだいぶ違うのです。
ドメインモデル図
ドメインモデル図は、色々調べたり試行錯誤した結果の我流になりますが、私はこんな感じで書きます。
- クラス図の簡易版のようなものを作成する
- 属性は代表的なものだけで良い、メソッドはなくて良い
- ドメイン知識(ルール、制約)をドメインモデルに吹き出しの形で表現する
- 集約の境界を決定し、オブジェクトを囲む
- 集約またぎの場合は、オブジェクト同士のhas-a関係ではなく、必ずIDの参照の形にする
これは、最終的に実装時に基本的にこのままEntity、もしくはValueObjectになります。
今回の図は以下のようになりました。
これを実装に落とします。
ここで、 先ほどの吹き出しの知識がそれぞれ吹き出しがついたオブジェクトの中に実装されている ことがポイントです。
この吹き出しはただのメモではなく、知識がどのオブジェクトのもので、どのオブジェクトに実装するべきかまで定めているのです。
コード
class Contract {
private CapacityPlan capacityPlan;
private List<OptionPlan> optionPlans;
public Contract(@NonNull CapacityPlan capacityPlan,
@NonNull List<OptionPlan> optionPlans) {
// 選択時にオプションプランの設定可能条件をチェックする
validatePlan(capacityPlan, optionPlans);
this.capacityPlan = capacityPlan;
this.optionPlans = optionPlans;
}
private void validatePlan(@NonNull CapacityPlan capacityPlan,
@NonNull List<OptionPlan> optionPlans) {
optionPlans.forEach(plan -> {
if (!plan.getPermittedCapacityPlans().contains(capacityPlan)) {
throw new NotPermittedCapacityPlanException("許可されていないプランです");
}
}
);
}
MonthlyTotalPrice calculateTotalPrice() {
// 契約で選択したプランに応じて請求金額を計算する
// capacityPlanのPriceとoptionPlansすべてのPriceを足し合わせる
Price sumPrice = this.optionPlans.stream()
.reduce(this.capacityPlan.getPrice(), (sum, plan) -> plan.getPrice(), Price::plus);
return new MonthlyTotalPrice(sumPrice);
}
}
class NotPermittedCapacityPlanException extends RuntimeException {
public NotPermittedCapacityPlanException(String msg) {
super(msg);
}
}
@Getter
enum CapacityPlan {
_1GB(1000),
_3GB(3000),
_30GB(6000);
// プランごとに月額が決まっている
private Price price;
CapacityPlan(int price) {
this.price = new Price(price);
}
}
@Getter
enum OptionPlan {
/**
* 動画無制限プラン
*/
MovieFree(
1000,
Arrays.asList(CapacityPlan._3GB, CapacityPlan._30GB)
),
/**
* 電話し放題プラン
*/
CallFree(
3000,
Arrays.asList(CapacityPlan._1GB, CapacityPlan._3GB, CapacityPlan._30GB)
);
private Price price;
// オプションは設定できる容量プランが決まっている
private List<CapacityPlan> permittedCapacityPlans;
OptionPlan(int price, List<CapacityPlan> permittedCapacityPlans) {
this.price = new Price(price);
this.permittedCapacityPlans = permittedCapacityPlans;
}
}
class Price {
private int value;
Price(int value) {
this.value = value;
}
int intValue() {
return value;
}
Price plus(Price price) {
return new Price(this.value + price.intValue());
}
}
@Getter
class MonthlyTotalPrice {
private Price value;
MonthlyTotalPrice(Price price) {
this.value = price;
}
}
一応githubにもあげました
注意として、契約オブジェクトがRepositoryを通じて永続化される場合は、契約オブジェクトは容量プラン、オプションプランのインスタンスではなくID参照になります。今回は永続化せずに選択したプランに応じてそのまま金額を計算する想定なのでこのようになりました。この理由はまた別の機会に。
【2021/10/30新刊】
DDD解説書の新刊を執筆しました。
重要トピック「モデリング」「集約」「テスト」について詳細に解説し、その他のトピックでは頻出の質問への回答と具体的なサンプルコードをふんだんに盛り込みました。現場で実践して、困っていることがある方はぜひこちらもご覧ください。