Scalaでのドメインモデリングのやりかた

 Scalaでのドメインモデリングのやりかた

実装コードを中心に、ドメインモデルの設計に対する考え方や改善方法についてまとめた資料です。

933291444e456bfb511a66a2fa9c6929?s=128

かとじゅん

November 09, 2018
Tweet

Transcript

  1. Scala でのドメインモデリングのやり方 Scala 関西Summit 20128 かとじゅん(@j5ik2o) Scala 関西Summit 2018 1

    / 40
  2. ChatWork テックリード github/j5ik2o scala­ddd­base scala­ddd­base­akka­http.g8 reactive­redis reactive­memcached 翻訳レビュー エリックエヴァンスのドメイン駆動設計 Akka

    実践バイブル 自己紹介 Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 2 / 40
  3. アジェンダ 以下のためのヒントを順不同で説明します 1. ドメインオブジェクトを見つけるには 2. ドメインオブジェクトを作るには 3. ドメインオブジェクトを育てるには Scala でのドメインモデリングのやり方

    Scala 関西Summit 2018 3 / 40
  4. ドメインモデルを生み出す、ドメイン分析は1980 年代はじめからあるらしい。 1995 年発刊のオブジェクト指向方法序説〈基盤編〉ジェームズ マーチン著で、ドメインとい う用語が登場する。 歴史を紐解きたい方は、こちらをごらんください。ドメインもしくはドメインモデルという概 念が登場する書籍一覧 FYI: ドメインという概念は昔からある

    Domain analysis ­ Wikipedia 引用: In software engineering, domain analysis, or product line analysis, is the process of analyzing related software systems in a domain to find their common and variable parts. It is a model of wider business context for the system. The term was coined in the early 1980s by James Neighbors. Domain analysis produces domain models using methodologies such as domain specific languages, feature tables, facet tables, facet templates, and generic architectures, which describe all of the systems in a domain. Several methodologies for domain analysis have been proposed. Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 4 / 40
  5. どこからはじめるか ドメインオブジェクトは、「システム」内部で利用される。 ドメインオブジェクトを知るには、システムを取り巻く環境を知 る必要がある ドメインに造詣が深くなければ、リレーションシップ要件駆動分析(RDRA) で要件を洗い出すことを推奨する リレーションシップ要件駆動分析(RDRA) 網羅的で整合性のある要件定義をUML の表現力を 使って、要件定義としてまとめる手法

    今回はRDRA の話はほとんどないです。知りたいかたは→ ドメインモデリングの始め方 ­ AWS DevDay Tokyo 2018 Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 5 / 40
  6. システム価値 コンテキストモデル 要求モデル システム外部環境 業務フロー 利用シーン 概念モデル システム境界 ユースケースモデル(今日はここから考える) 画面/

    帳票モデル(Optional) プロトコル/ イベントモデル(Optional) システム ドメインモデル データモデル システムを語るのに不可欠な要素 Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 6 / 40
  7. ユースケースからドメインモデルを考える カンファレンス告知サイト カンファレンス主催者が、カンファレンスを作成する カンファレンス主催者が、カンファレンスを公開する ユーザが、カンファレンスに参加する ユーザが、参加したカンファレンスをキャンセルする Scala でのドメインモデリングのやり方 Scala 関西Summit

    2018 7 / 40
  8. ユースケースの分析 凡例 A はアクター B はバウンダリ E はエンティティ( ドメインオブジェクト) C

    はコントロール ユースケース カンファレンス主催者(A) が、カンファレンス管理画面(B) からカンファレンス (E) を作成する(C) カンファレンス主催者(A) が、カンファレンス管理画面(B) からカンファレンス (E) を公開する(C) ユーザ(A) が、カンファレンス告知ページ(B) からカンファレンス (E) に参加する(C) ユーザ(A) が、カンファレンス告知ページ(B) から参加したカンファレンス (E) をキャンセルする(C) カンファレンス主催者(A) が、カンファレンス管理画面(B) からカンファレンス (E) の参加者一覧 (E) を確認する(C) CRUD よりも、ドメイン知識を表す判断・加工・計算を表すコントロールに注目する Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 8 / 40
  9. エンティティ カンファレンス( 集約) コントロール 作成する 公開する 参加する キャンセルする エンティティとコントロール Scala

    でのドメインモデリングのやり方 Scala 関西Summit 2018 9 / 40
  10. コードにする前に Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 10 / 40

  11. レイヤーを分割しよう レイヤーをプロジェクトとして分割し、依存の方向を制御してください 循環参照を作ってからでは遅いです! // インフラストラクチャ層 val infrastructure = (project in

    file("infrastructure")).settings(...) // ドメイン層 val domain = (project in file("domain")).settings(...).dependsOn(infrastructure) // ユースケース層 val useCase = (project in file("use-case")).settings(...).dependsOn(domain, infrastructure) // インターフェイス層 val interface = (project in file("interface")).settings(...).dependsOn(useCase, infrastructure) // アプリケーション本体 val boot = (project in file("boot")).settings(...).dependsOn(interface, infrastructure) val root = (project in file(".")).settings(...).dependsOn(boot) Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 11 / 40
  12. 最初のカンファレンス実装 これはドメインモデルを反映した実装といってよいか? case class Conference(id: Long, status: Int, name: String,

    limit: Int participants: Seq[Long]) MVC の父、Trygve Reenskaug 氏は、モデルは知識の表象と定義した。さて、どれが知識だろうか? Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 12 / 40
  13. 最初のカンファレンス実装 これはドメインモデルを反映した実装といってよいか? case class Conference(id: Long, status: Int, name: String,

    limit: Int participants: Seq[Long]) MVC の父、Trygve Reenskaug 氏は、モデルは知識の表象と定義した。さて、どれが知識だろうか? ドメイン知識の正体は、CRUD 以外の判断 / 加工 / 計算の振る舞いである。 このモデルにはそれがない。本当にないか?重要な 側面を見落としてないだろうか? Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 12 / 40
  14. 実際のコード表現 参加者の追加 val conference = Conference(id = 1L, status =

    1, name = "Scala 関西Summit 2018", limit = 5000, participants = Seq.empty) val newConfernece = conference.copy( participants = conference.participants :+ 10L /* userAccountId */ ) なんとかならないか。これがDDD の真骨頂? Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 13 / 40
  15. 実際のコード表現 参加者の追加 val conference = Conference(id = 1L, status =

    1, name = "Scala 関西Summit 2018", limit = 5000, participants = Seq.empty) val newConfernece = conference.copy( participants = conference.participants :+ 10L /* userAccountId */ ) なんとかならないか。これがDDD の真骨頂? ちがいます !!! Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 13 / 40
  16. ドメインモデルに知識を集約する ユースケースからCRUD 以外の判断/ 加工/ 計算の関心事を抜き出し割り当てる // 作成するはインスタンスを作成する? case class Conference(id:

    ConferenceId, name: ConferenceName, ... ) { // 非公開と公開で何が違うのか?状態が違うだけか? def publish: Conference = ... // 参加するの定義とは? def addParticipants(...): Conference = ... // キャンセルじゃなくて、参加者の削除? def removeParticipants(...): Conference = ... // " の" で接続する用語は集約の属性になることが多い def getParticipants: Participants = ... } 曖昧な部分がかなりある… 。 保持する値は値オブジェクトとして実装。 case class ConferenceId(value: Long) { require(value > 0L) // コード表現に変換できる def asCode: Code = Code(value) } Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 14 / 40
  17. 集約の境界定義 カンファレンスの中に何が含まれるのか? カンファレンスは特定される必要があるので、何らかの識別子が必要 カンファレンスのタイトル カンファレンスの場所 カンファレンスの開催時期 カンファレンスの主催者 カンファレンスの参加者 ユースケースだけに頼り切らない。モデルの整合性が成り立つかも検証する Scala

    でのドメインモデリングのやり方 Scala 関西Summit 2018 15 / 40
  18. エンティティ=集約ではない 集約は、一つ以上のエンティティと値オブジェクトを包括する 集約ごとにリポジトリを実装する 強い整合性の境界を持つ 隣り合う属性は、強い関連性があり、同じタイミングで更新さ れる場合、同一の境界としてふさわしい。 顧客の名前と住所の変更は同じようなタイミングで更新さ れる、など 上記条件を外れる場合は、適切な集約を見つけて所有権を持つ 集約を変更し、整合性は結果整合を選択する

    FYI: 集約とリポジトリの関係 Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 16 / 40
  19. 参加者は内部か外部か? Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 17 / 40

  20. どちらがよいか? Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 18 / 40

  21. メリット 関連の増大を軽減できる 参加者上限を厳密に守ることができる デメリット 大量の参加者を扱うことは現実的ではない 大きい集約はロックも大きくなる // カンファレンス集約 case class

    Conference(id: ConferenceId, status: ConferenceStatus, name: ConferenceName, limit: ParticipantLimit, participants: Set[Participat]) { // 不変条件の表明 require(limit.isLessThan(participants)) // 閉じた操作によって、コンストラクタで不変条件を検証する def addParticipants(otherParticipants: Set[Participant]): Conference = copy(participants = participants ++ otherParticipants) } 参加者を含むカンファレンス集約 Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 19 / 40
  22. メリット ID を用いて単独で検索できる 参加者の件数が多い場合でも対応できる デメリット 弱い整合性になるため、カンファレンスが読めるときに、参加者が読める とは限らない。その逆も。 厳密な参加者上限を設けることができない( チェック・ゼン・アクト操作に 割り込み発生するなど)

    // カンファレンス集約 case class Conference(id: ConferenceId, status: ConferenceStatus, name: ConferenceName, limit: ParticipantLimit) { // ... } // 参加者集約 case class Participant(id: ParticipantId, ConferenceId: ConferenceId, userAccountId: UserAccount) { // ... } 参加者は独立した集約 Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 20 / 40
  23. ロックの衝突が起きやすくなる ①→ ②→ ③ ②: ①のロックと衝突・更新の失敗 ②→ ③→ ① ③①:

    ②のロックの衝突・更新の失敗 ③→ ①→ ② ②: ③と①ロックの衝突・更新の失敗 複数の集約を更新する操作をRDB などのトランザクション で束ねることは、結局巨大な集約を定義しているに等し い。また、境界定義もユースケースによって変動してしま うため、モデルの知識だけでは整合性について把握するこ とが難しくなる 対策についてはアンカンファレンスで話しましょう (笑 FYI: こんなユースケースは要注意 Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 21 / 40
  24. カンファレンスの状態 カンファレンスの下書き状態 もしくは 公開状態か 公開した後に、もう一度非公開にすることもある… 状態には、非公開と公開の状態があればいいことがわかった 整理できていないなら、状態遷移図を書くといいかも case class Conference(id:

    ConferenceId, status: ConferenceStatus, limit: ParticipantLimit, name: ConferenceName, ...) { // ... } import enumeratum._ sealed trait ConferenceStatus extends EnumEntry object ConferenceStatus extends Enum[ConferenceStatus] { val values = findValues case object Private extends ConferenceStatus case object Public extends ConferenceStatus } Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 22 / 40
  25. 以下のように集約の型( トップレベル) を別々にしてしまうと、同一集合と して扱えなくなります。 case class PrivateConference(id: PrivateConferenceId, ...) case

    class PublicConference(id: PublicConferenceId, ...) リポジトリは集約の型ごとに定義されるので、 class PrivateConferenceRepository { ... } class PublicConferenceRepository { ... } 別々の集合を扱うことになる val privateConferences = privateRepository.resolveByOwnerId(ownerId) val publicConferences = publicRepository.resolveByOwnerId(ownerId) 問題 : 状態ごとに集約の型を定義すると ... Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 23 / 40
  26. 集約のトップレベルの型を変えずに、サブ型を導入する sealed trait Conference { ... } case class PrivateConference

    extends Conference { ... } case class PublicConference extends Conference { ... } // トップレベルに対するリポジトリを定義する class ConferenceRepository { ... } すべてひとつの集合として扱うことができる val conferences: Seq[Conference] = conferenceRepository .resolveByOwnerId(ownerId) // 部分集合を取得する問い合わせメソッドがあってもよい val conferences: Seq[PrivateConference] = conferenceRepository .resolvePrivateByOwnerId(ownerId) Conferences.foreach { case Conference: PrivateConference => // ... case Conference: PublicConference => // ... } 解決 : 集約の型にサブ型を導入する Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 24 / 40
  27. VO で暗黙的な概念を明示的にする ドメイン知識の正体は、判断/ 加工/ 計算の処理。以下のような判断を値オブジェクトの責務にする require(limit >= 1) require(limit >=

    participants.size) ↓ 参加者上限という、暗黙的な概念を明示的にするリファクタリング require(limit.isLessThen(participants)) 参加者の上限についてはParticipantLimit の中に集約できる(DDD 仕様パターン) case class ParticipantLimit(value: Int) { require(value >= 1) def isLessThen(participants: Seq[Participant]): Boolean = if (value >= participants.size) true else false } Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 25 / 40
  28. コレクション操作の設計を改善する コレクションを操作する処理は、複雑・冗長・見通しが悪くなりがち。設計の意図が汲み取りづらい // 非ファーストクラスコレクション case class Item(id: Long, price: Long)

    val items: Set[Item] = Set(...) val totalPrice: Long = items.foldLeft(0L)(_ + _.price) val filteredByPrice: Set[Item] = items.filter(_.price > 10) ファーストクラスコレクションに集約し、意図を明確にする // ファーストクラスコレクション case class Items(values: Seq[Item]) { def totalPrice: Long = values.foldLeft(0L)(_ + _.price) def filteredByPrice(price: Long): Seq[Item] = values.filter(_.price > price) } val items: Items = Items(...) val totalPrice = items.totalPrice val filteredByPrice = items.filteredByPrice(10L) Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 26 / 40
  29. 閉じた操作の導入で凝集度を高める DDD のしなやか設計に含まれるパターン: 閉じた操作(CLOSURE OF OPERATIONS) 半群が持つ性質をモデリングに利用している add はItems にしか依存しないので、余計な概念の混入さけ凝集度を高めて変更に強くできる

    case class Items(values: Seq[Item]) { def add(other: Items): Items = copy(values = values ++ other.value) } Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 27 / 40
  30. 問題 :Getter によるロジックの流出・分散 プリミティブ型をGet して操作するから、ドメイン知識がドメインオブジェクトから流出・分散する case class Conference(id: CategoryId, category:

    String, ...) val conference = Conference(id = ..., category = "IT/Database/HBase", ...) ちらばったドメインロジックはドメイン層から離れたところに… 。 val categorySplits = conference.category.split("/") val root = categorySplits.headOption // ルートカテゴリの取得? val parent = categorySplits(categorySplits.size - 1) // 親カテゴリの取得? 無用に複雑で意図も汲み取りにくい Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 28 / 40
  31. 解決 : 値オブジェクトへのロジックの集約 プリミティブ型をGetter で返さない Get して操作するロジック( 判断/ 加工/ 計算)

    をドメインオブジェクトに集約する case class Category(path: String, parent: Option[Category] = None) { require(path.nonEmpty) require(path.size <= 255) def root: Option[Category] = { @tailrec def loop(current: Category): Option[Category] = current.parent match { case None => Some(current) case Some(p) => loop(p) } loop(this) } def asString: String = parent.map(_.asString).fold(path)(v => s"$v/$path") } case class Conference(..., category: Category, ...) val root = conference.category.root // ルートカテゴリを返す val parent = conference.category.parent // 親カテゴリを返す val path = conference.category.asString // ルートカテゴリを返す Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 29 / 40
  32. Getter を使わざるを得ない場合 JSON に変換するなど用途でどうしてもGetter が必要な場合がある プロパティに長い名前を使うことでロジックの流出を感知できるようにする case class Items(breachEncapsulationOfValues: Seq[Item])

    { private val values = breachEncapsulationOfValues def totalPrice: Long = values.foldLeft(0L)(_ + _.price) def filteredByPrice(price: Long): Seq[Item] = values.filter(_.price > price) } Getter を使うコードがあれば、リファクタリングのチャンス items.breachEncapsulationOfValues.filter(_.price == 100L).foldLeft(0L)(_ + _.price) ↓ def totalPrice(price: Long): Long = values.filter(_.price > price).foldLeft(0L)(_ + _.price) Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 30 / 40
  33. FYI: エンティティの責務と構造 エンティティの主たる責務は、同一性・連続性。値を保持することが主ではない 次の例は、責務にふさわしくない trait Employee { val id: EmployeeId

    val name: EmployeeName val emailAddress: EmailAddress val pref: Pref val cityName: CityName val addressName: AddressName val buildingName: Option[BuildingName] } 値オブジェクトに集約することが望ましい。振る舞いも値オブジェクトに移動しよう trait Employee { val id: EmployeeId val profile: EmployeeProfile } Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 31 / 40
  34. 最終的なカンファレンスの実装 // 参加者 case class Participant(userAccountId: UserAccountId, createdAt: ZonedDateTime) {

    // equals, hashCode は userAccountId のみに基づく } // ファーストクラスコレクションとしての参加者の集合 case class Participants(breachEncapsulationOfValues: Set[Participant]) { require(values.nonEmpty) private val values = breachEncapsulationOfValues // 閉じた操作。他の型と結合できなくなるパターン def add(other: Participants): Participants = copy(values = values ++ other.values) def size: Int = values.size } // カンファレンス case class Conference(id: ConferenceId, status: ConferenceStatus, category: Category, name: ConferenceName, limit: ParticipantLimit, participants: Participatns) { // 不変条件の検証 require(limit.isLessThen(participants)) // 閉じた操作 def addParticipants(participants: Participant*): Conference = copy(participants = participants.add(participants)) } Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 32 / 40
  35. 実際のコード表現 改善前 val conference = Conference(id = 1L, status =

    1, name = "Scala 関西Summit 2018", limit = 5000, participants = Seq.empty) val newConfernece = conference.copy(participants = conference.participants.copy(participants = conference.participants + 10L /* userAccountId */) ) ) 改善後 val conference = Conference(id = ConferenceId(1L), status = ConferenceStatus.Private, name = ConferenceName("Scala 関西Summit 2018", en = "Scala Kansai Summit 2018), limit = PerticipantLimit(5000), paticipants = Participants.empty) val newConference = conference.addParticipants(Particitpant(userAccountId = 10L)) Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 33 / 40
  36. 予測可能な設計を目指す 1. できる限りクライアント目線で意図が明白になるI/F を設計する 2. 結果が予測しやすいように、副作用のない関数(Scala は問題になることが少ない) 3. 不変条件の表明も忘れずに Scala

    でのドメインモデリングのやり方 Scala 関西Summit 2018 34 / 40
  37. 意図の明白なインタフェース 問題 モデルの使うために実装を見なければわからないようであれば、カプセル化が正しく機能しているとは言えない モデル設計の意図がわかりにくければ、誤った使い方をしてしまうかもしれない 解決 クラスやプロパティ、メソッドの名前には、必ずユビキタス言語を利用する TDD のように、利用者の視点から命名することも助けになる describe(" カンファレンスについて")

    { it(" カンファレンスに参加者を追加できる") { conference1.addParticipants(Particitpant(userAccountId)) shouldBe expectedConference } } Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 35 / 40
  38. 副作用のない関数 (1/2) 問題 副作用が伴う操作は結果の予測が難しい 結果的に実装を理解しなければならなくなる 安全に組み合わでできないため、表現力が低下する 解決 引数を加工して結果を返すだけのロジックはできるだけ副作用のない関数化する 状態を変化させる操作( コマンド)

    は最小限に留める Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 36 / 40
  39. 副作用のない関数 (2/2) Scala ではデフォルトが不変なので意識しなくても実践できるが、mutable コレクションなどの可変オブジェクトや、Actor を使 う場合は要注意 class EmployeActor(id: EmployeeId)

    extends Actor { private var state: Option[Employee] = None override def receive: Receive = { // ... case UpdateDeptId(id, deptId) if this.id == id && state.nonEmpty => // 状態の可変は最小限に留める state = state.map(_.withDeptId(deptId)) // ... } } case class Employee(id: EmployeeId, name: EmployeName, deptId: DeptId, ...) { // 副作用のない関数 def withDeptId(value: DeptId): EmployId = copy(deptId = value) // copy メソッドも隠蔽したい場合は、case class やめる… 。 } Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 37 / 40
  40. 表明 不変条件の表明を行う { INV & P } A { INV

    & Q } Immutable の場合は INV は、コンストラクタで行う case class Conference(id: ConferenceId, status: ConferenceStatus, category: Category, name: ConferenceName, limit: ParticipantLimit, participants: Participatns) { // 不変条件の検証 require(limit.isLessThen(participants)) def addParticipants(participants: Participant*): Conference = { // ここでのINV チェックは不要 copy(participants = participants.add(participants)) // ここでのINV チェックは不要 } } コンパイル時の表明として、型で不変条件を表現する方法もある final case class NonEmptyList[+A](file:///Users/j5ik2o/Source/slides/domain-modeling-in-scala-scala-ks-2018/head: A, tail: List[A]) .. Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 38 / 40
  41. まとめ 重要なことは、モデルを軸にコミュニケーションできる状態にすること すくなくともユビキタス言語を含むユースケースで相互理解できること ドメインモデルもユースケースから探査することで見つけることができる この前提のもとで、今回の実装テクニックを生かすと、有益。 集約の境界定義 VO を有効活用する ファーストクラスコレクション Getter

    には気をつける 予測可能な設計を心がける Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 39 / 40
  42. 一緒に働くエンジニアを募集しています! http://corp.chatwork.com/ja/recruit/ Scala でのドメインモデリングのやり方 Scala 関西Summit 2018 40 / 40