little hands' lab

ドメイン駆動設計を布教したい

現場でDDD!のハンズオン、持ち帰ってやってみた

genbade-ddd.connpass.com

こちらのイベントに参加してきました。
タイトルに「レガシーをぶっつぶせ」とあった効果か、基本レガシーに立ち向かった実体験ベースの具体的な、泥臭い話が多くて非常に楽しかったです。

DDD周りって結構抽象的な話に終始してしまうことが多いので、これほど具体的な話が集まったイベントはすごく貴重だったと思います。人数も相当集まっており相当好評だったようでまたやりたい、とクロージングで話されていたので、また次回に期待したいです。

モデリングハンズオン

その中で、「DDDモデリングハンズオン」というコンテンツがあり、モデリングしながらドライバーの方がライブでコードを書いていただくというとても良いコンテンツがあったので参加してきました。

そこでは携帯の月額プランをお題にモデリングをされていました。

そこで参加した内容が時間内に終わらなかったこともあり、自分でも手を動かしてみたくなったので自分の普段のやり方でモデリングしてみました。

ドメインモデリングをどういう風にするか、というのをブログに書いたこともなかったので、せっかくなので紹介させてくださいm( )m

ユースケース図

モデリングする際は、僕は必ずユースケース図から始めます。

モデリングというのは何か問題を解決するために現実世界の一部を抽象化することなので、どのように使用するのかを定めなければモデリングの目的がわからなくなってしまいます。

ユースケース図を書くことで、モデリング参加者の間でモデルの用途の認識が揃います。また、その会でモデリングするスコープを限定する効果もあります。体外に置いてシステム化したいものは多くあるので、スコープを切らないと際限なく広がってしまい話が発散してしまいがちなのです。

ということで、今回はこんな感じに切ってみました。シンプルですね。
でもシンプルでもあるだけでだいぶ違うのです。

f:id:little_hands:20190511213734p:plain:w300

ドメインモデル図

ドメインモデル図は、色々調べたり試行錯誤した結果の我流になりますが、私はこんな感じで書きます。

  • クラス図の簡易版のようなものを作成する
  • 属性は代表的なものだけで良い、メソッドはなくて良い
  • ドメイン知識(ルール、制約)をドメインモデルに吹き出しの形で表現する
  • 集約の境界を決定し、オブジェクトを囲む
    • 集約またぎの場合は、オブジェクト同士のhas-a関係ではなく、必ずIDの参照の形にする

これは、最終的に実装時に基本的にこのままEntity、もしくはValueObjectになります。

今回の図は以下のようになりました。

f:id:little_hands:20190511223901p:plain

これを実装に落とします。

ここで、 先ほどの吹き出しの知識がそれぞれ吹き出しがついたオブジェクトの中に実装されている ことがポイントです。

この吹き出しはただのメモではなく、知識がどのオブジェクトのもので、どのオブジェクトに実装するべきかまで定めているのです。

コード


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にもあげました

Javaコード
ドメインモデル図(PlantUML)

注意として、契約オブジェクトがRepositoryを通じて永続化される場合は、契約オブジェクトは容量プラン、オプションプランのインスタンスではなくID参照になります。今回は永続化せずに選択したプランに応じてそのまま金額を計算する想定なのでこのようになりました。この理由はまた別の機会に。

終わりに

TwitterでもDDDの話をつぶやいてるのでよろしければフォローしてください。
記事に関するご質問もお気軽にどうぞ!

松岡@DDDブログ書いてます (@little_hand_s) | Twitter