Slide 1

Slide 1 text

2017-09-09 実践ScalaでDDD 1 at Mt.Fuji 2014 実践 ScalaでDDD DDD on Scala

Slide 2

Slide 2 text

“ 2017-09-09 実践ScalaでDDD 2 辻 陽平 Yohei TSUJI ” フリーランスのプログラマ - Java 17年くらい、Scala 2年弱 - 主にエンタープライズ系システムの ソフトウェアアーキテクトを担当 - 現在は Scala / AWS / DDD で クラウドサービスの開発中 - 車好き、F1好き、たまに登山 Twitter / GitHub / Qiita @crossroad0201

Slide 3

Slide 3 text

このセッションについて 2017-09-09 実践ScalaでDDD 3 こんなエンジニアのみなさまに… - DDD(ドメイン駆動設計)は実践していて、Scalaが気になっている。 - Scalaは使っていて、DDDを導入しようと思っている。 - ScalaもDDDもやってないが、良さそうならやってみたい。 - ScalaもDDDもやっていて、なにか役に立ちそうな情報があれば….

Slide 4

Slide 4 text

コンテンツ 2017-09-09 実践ScalaでDDD 4

Slide 5

Slide 5 text

1. DDDとは What is DDD 2017-09-09 実践ScalaでDDD 5

Slide 6

Slide 6 text

DDD(ドメイン駆動設計)とは 2017-09-09 実践ScalaでDDD 6 オブジェクト指向で 変更が容易なソフトウェア を開発するための 体系化された原則集 - 顧客も開発者も、共通の「ユビキタス言語」で会話。 - 業務の関心事で「コンテキスト」を分割する。 - ソフトウェアを構成するコンポーネントとその責務の定義。 …etc

Slide 7

Slide 7 text

DDD(ドメイン駆動設計)とは 2017-09-09 実践ScalaでDDD 7 ドメインの知識を ドメインの言葉で コードに落とし込む - ドメイン・・・システム化しようとする対象業務 - ドメインの言葉・・・ユビキタス言語

Slide 8

Slide 8 text

DDD(ドメイン駆動設計)とは 2017-09-09 実践ScalaでDDD 8 エリック・エヴァンスのドメイン駆動設計 実践ドメイン駆動設計 なにはともあれ… ただし、関数型言語で副作用を起こさずにDDDを実践するためには 解釈の仕方や設計に工夫が必要…

Slide 9

Slide 9 text

DDDの登場人物 (コンポーネント) 2017-09-09 実践ScalaでDDD 9

Slide 10

Slide 10 text

DDDのコンポーネント(1 / 2) 2017-09-09 実践ScalaでDDD 10 集約 ドメインの中で、整合性を 保証しなければならない まとまり。 ファクトリ 「エンティティ」を新しく作 成する。 エンティティ 「集約」の中で、一意な識 別子で識別される概念。 通常は集約の中に1つ。 リポジトリ 「エンティティ」をデータス トアに永続化し、データス トアから「エンティティ」を 取得する。 バリューオブジェクト 「集約」の中で、識別子で 識別する必要がない値。 エンティティに従属する。 ドメインサービス ドメインのビジネスルー ル(業務の仕様)を実行 する。

Slide 11

Slide 11 text

CQRS(Command Query Responsibility Segregation) DDDのコンポーネント(2 / 2) 2017-09-09 実践ScalaでDDD 11 ドメインイベント ドメインで発生する、ビジ ネス上の事象を表現する オブジェクト。 イベントサブスクライバ 「ドメインイベント」を受信 して「クエリモデル」を更 新し、ビューを最新に保 つ。 イベントパブリッシャ 「ドメインイベント」をメッ セージキューに発行する。 クエリプロセッサ 「クエリモデル」を検索す る。 クエリモデル ドメインをある切り口で見 た読み取り専用のビュー。 複数の「集約」を合成した ビューも定義できる。 アプリケーション サービス ドメインの各コンポーネン トを使って、ユースケース を実現する。

Slide 12

Slide 12 text

2. Scalaと DDD Scala with DDD 2017-09-09 実践ScalaでDDD 12

Slide 13

Slide 13 text

Scala と DDD は… 2017-09-09 実践ScalaでDDD 13

Slide 14

Slide 14 text

相性がいい! 2017-09-09 実践ScalaでDDD 14

Slide 15

Slide 15 text

ドメインの仕様を自然に表現できる 2017-09-09 実践ScalaでDDD 15 // ユーザを取得して、もし存在しなければエラー val user = userRepository.get(userId) ifNotExists NotFoundError - レシーバー と メソッド の区切りに半角スペースが使える。 - メソッドの引数が1つだけなら、( ) を省略できる。 - もともとの言語仕様かのように、表現力を拡張していける。 コードが自然文(英文)の仕様書になる。

Slide 16

Slide 16 text

オブジェクト指向と関数型 2017-09-09 実践ScalaでDDD 16 ドメインの静的側面(概念モデル)は オブジェクト指向で。 動的側面(ユースケース)は関数型で。 両方のいいとこ取りができる。 静的な型を持つオブジェクト指向言語であり、関数型言語でもある。 case class User(id: UserId, name: UserName) { val renameTo: UserName => User = name => copy(name = name)

Slide 17

Slide 17 text

case classで簡単に型が作れる 2017-09-09 実践ScalaでDDD 17 case class User(id: UserId, name: UserName) - コンストラクタは定義しなくていい。 - Getter / Setter も定義しなくていい。 - #equals()、#hashCode()、#toString() のデフォルト実装。 - #copy() メソッドでイミュータブルなプログラミングが楽。 大量の型を作るDDDではコード量削減効果大

Slide 18

Slide 18 text

変数と関数の区別がない 2017-09-09 実践ScalaでDDD 18 class Example { val foo: String = … } new Example().foo プロパティ <-> 導出プロパティの変更が容易 class Example { def foo: String = … } new Example().foo // 影響なし

Slide 19

Slide 19 text

関数(メソッド)をネストできる 2017-09-09 実践ScalaでDDD 19 def remindTodayScheduleTo(id: UserId) = { def findUser = { … } def getScheduleOf(user: User) = { … } def sendReminderTo(user: User, schedule: Schedule) = { … } 処理ステップに、細かく名前を付け易い。 - その関数を使う場所(スコープ)が明確。 - ネストした関数から、外側の関数の引数を参照できる。 - private メソッドだらけにならない。

Slide 20

Slide 20 text

Scala programming style 3.Scala実装 スタイル 2017-09-09 実践ScalaでDDD 20

Slide 21

Slide 21 text

2017-09-09 実践ScalaでDDD 21 // イミュータブル case class User(name: UserName) { def renameTo(name: UserName): User = copy(name = name) } イミュータブルに作る // ミュータブル class User(var name: UserName) { def renameTo(name: UserName): Unit = this.name = name } - var(再代入可)は使わず、val(再代入不可)を使う。 case class にすると自動的に val に。 - 値を書き換えるのではなく、値の異なる別インスタンスを作って返す。 case class の copy() メソッドで簡単に実装できる。

Slide 22

Slide 22 text

2017-09-09 実践ScalaでDDD 22 副作用を起こさない(局所化する) - 戻り値として返らないものはすべて副作用。 - ソフトウェアでは必ず何らかの永続化処理(副作用)を伴うので、副 作用を完全に無くすことはできないが、副作用を起こすコードを局所 化することはできる。 - DDDにおいては、コアとなる ドメインロジック から副作用を排除する ことが重要。

Slide 23

Slide 23 text

2017-09-09 実践ScalaでDDD 23 object User { def create(…) = { // Userを生成して返すだけ User(…) } } val user = User.create(…) userRepository.save(user) 副作用を起こさない(局所化する) object User { def create(…) = { val user = User(…) // Userの生成と同時に保存 // 保存処理が副作用になる userRepository.save(user) } } val user = User.create(…)

Slide 24

Slide 24 text

2017-09-09 実践ScalaでDDD 24 def remindTodayScheduleTo(id: UserId) = { def findUser = { … } def getTodaysScheduleOf(user: User) = { … } def sendReminderTo(user: User, schedule: Schedule) = { … } for { user <- findUser todaysSchedule <- getTodaysScheduleOf(user) sentResult <- sendReminderTo(user, schedule) } yield sentResult } for内包式を使いこなす ローカル関数を定義して 処理ステップに名前を付ける for内包式で処理ステップをつなげて 処理フローを作る

Slide 25

Slide 25 text

2017-09-09 実践ScalaでDDD 25 object User { def create(name: UserName)(implicit idGenerator: IDGenerator) = {…} } implicit val idGenerator: IDGenerator = … val user = User.create("WHO") // idGeneratorが自動的に渡される implicitパラメタを活用する - ドメインの関心事ではないパラメタ(DBセッションなど)は、 implicit parameter で暗黙的に渡すことでドメインロジックから ノイズを排除する。

Slide 26

Slide 26 text

2017-09-09 実践ScalaでDDD 26 case classのcopy()は外から使わない - case classに自動生成される copy() メソッドを乱用すると、 コードの意図がわからなくなるので、自クラス内でのみ使用し、 外からは直接使用しない。 case class User(name: UserName) { def renameTo(name: UserName): User = copy(name = name) } user.renameTo("Taro") case class User(name: UserName) // 何を意図しているのかわからない user.copy(name = "Taro")

Slide 27

Slide 27 text

2017-09-09 実践ScalaでDDD 27 for { user <- findUser todaysSchedule <- getTodaysScheduleOf(user) sentResult <- sendReminderTo(user, schedule) } yield sentResult 変数名はむやみに略さない for { u <- findUser s <- getTodaysScheduleOf(u) r <- sendReminderTo(u, s) } yield r 関数型言語の文化では、 変数名を x, y などに略し がち… むやみに略すとコードが 数学の方程式のように なって読みづらくなる。

Slide 28

Slide 28 text

2017-09-09 実践ScalaでDDD 28 def findUser: Either[Option[User]] = { … } for { // Option[User] maybeUser <- findUser } yield for { // User user <- maybeUser : } yield … Option型の変数はmaybeを付ける - 値がないかもしれない変数 (Option型)は、変数名に maybeを付けるとわかりやす い。 - Scalaでは型推論で型を省略 することが多いので、Option かどうかが変数名からパッと 見てわかると便利。

Slide 29

Slide 29 text

2017-09-09 実践ScalaでDDD 29 for { user <- findUser todaysSchedule <- getTodaysScheduleOf(user) sentResult <- sendReminderTo(user, schedule) } yield sentResult 自動コードフォーマッタを利用 for { user <- findUser todaysSchedule <- getTodaysScheduleOf(user) sentResult <- sendReminderTo(user, schedule) } yield sentResult - Scalafmt や Scalareform のプラグインをsbtに組み 込んで使う。 - 特に for内包式 と match 式は自動フォーマットで 揃えると、コードがグッと 読みやすくなる。

Slide 30

Slide 30 text

4.アーキ テクチャ Architecture 2017-09-09 実践ScalaでDDD 30

Slide 31

Slide 31 text

レイヤ構成 2017-09-09 実践ScalaでDDD 31

Slide 32

Slide 32 text

レイヤと責務 2017-09-09 実践ScalaでDDD 32 ドメインの知識をモデル化。 名詞句は型(クラス)に、動詞句は振る舞 い(メソッド)として実装する。 ドメインモデル の状態を永続化する。 永続化処理は 副作用 になる。 ドメインモデル を使用して、ユースケース を実現する。 Web画面 や REST API、Command Line Interface など、ユースケースを起動するた めのユーザーインターフェースを提供する。 インフラストラクチャ レイヤ インターフェース レイヤ アプリケーション レイヤ ドメイン レイヤ

Slide 33

Slide 33 text

レイヤ構成 2017-09-09 実践ScalaでDDD 33 インターフェース アプリケーション ドメイン インフラストラクチャ レイヤ化アーキテクチャ ドメイン が インフラストラクチャ に依存してしまう。 - ドメイン が副作用を起こしてしまう。 - ドメイン のコードに、インフラストラクチャ の コードが混在してしまう。 - ミドルウェア や サービス、データストレージ を 柔軟に変更できない。 - ドメイン のユニットテスト がやりにくい。

Slide 34

Slide 34 text

レイヤ構成 2017-09-09 実践ScalaでDDD 34 インターフェース アプリケーション ドメイン インフラストラクチャ ドメイン中心アーキテクチャ ドメイン が どのレイヤにも依存し ない。 - ドメイン から副作用を排除。 - ただし、アプリケーション は インフラストラクチャ に依存し たまま。

Slide 35

Slide 35 text

レイヤ構成 2017-09-09 実践ScalaでDDD 35 オニオンアーキテクチャ The Onion Architecture : part 1 http://jeffreypalermo.com/blog/the -onion-architecture-part-1/ - ヘキサゴナルアーキテクチャ も 基本的な考え方は同じ。 - クリーンアーキテクチャは、より GUIアプリに特化?

Slide 36

Slide 36 text

レイヤ構成 2017-09-09 実践ScalaでDDD 36 インターフェース アプリケーション ドメイン インフラストラクチャ ドメイン も アプリケーション も イ ンフラストラクチャ に依存しない。 - インターフェース で、 アプリケーションとドメイン に インフラストラクチャ を合成 (依存性注入)して使う。 オニオンアーキテクチャ

Slide 37

Slide 37 text

エラー処理 2017-09-09 実践ScalaでDDD 37

Slide 38

Slide 38 text

エラー処理 2017-09-09 実践ScalaでDDD 38 Scalaでは例外をスローしない - 関数型プログラミングでは、戻り値として返らない例外すら副作用。 - エラー処理では例外をスローするのではなく、 戻り値を 正常 or エラー でラップして返す。 - 戻り値をラップする型を揃えることで、エラー処理が書きやすくなる。

Slide 39

Slide 39 text

エラー処理 2017-09-09 実践ScalaでDDD 39 Option Some(値がある) or None(値がない) のいずれか。 Try Success(成功) or Failure(失敗) のいずれか。 予測不能なエラーに 使う。 Either Right(正常) or Left(異常) のいずれか。 予測可能なエラーに 使う。 Scalaの値をラップする型

Slide 40

Slide 40 text

エラー処理 2017-09-09 実践ScalaでDDD 40 scala.Option ドメインの概念上、あったりなかったりする値を表現するために使う。 エラーの内容を表現できないので、エラー処理では使わない。 - リポジトリ の取得系メソッドで、該当する エンティティ が存在しない かもしれないとき。 - エンティティ で、値が任意のプロパティ。 def getUser(id: UserId): Option[User] = { … }

Slide 41

Slide 41 text

エラー処理 2017-09-09 実践ScalaでDDD 41 scala.util.Either 異常時に、任意のエラー値を返すことができる。 単純な String のエラーメッセージやエラーコードを返すのではなく、独 自にエラーオブジェクトを作って返すようにする。 - ドメイン や アプリケーション のエラー処理は Either で。 - 独自のエラーオブジェクトにもスタックトレースを持たせておいたほ うが、エラーの発生箇所の特定が容易。 def assign(id: UserId): Either[TaskAlreadyClosed, Task] = { … }

Slide 42

Slide 42 text

エラー処理 2017-09-09 実践ScalaでDDD 42 scala.util.Try Javaの例外(Throwable)をラップして戻り値として返せるようにする。 例外が発生した場合、関数の戻り値が Failure(Throwable) になる。 - 例外が発生し得る処理を Try { … } で囲む。 - インフラストラクチャ では Javaのライブラリ(JDBCドライバなど)を使 うことが多く、エラー時は例外がスローされるので、インフラストラク チャでは Try を使うと楽。 def save(user: User): Try[User] = { … }

Slide 43

Slide 43 text

エラー処理 2017-09-09 実践ScalaでDDD 43 各レイヤで発生するエラーの特性 ドメインルールの違反など、ユーザーの操 作に起因するエラー。 データストレージやネットワークの障害、連 携する外部サービスのダウンなど、ユー ザーに起因しない想定外のエラー。 ユースケースの実行が継続できない場合 にエラー。 主にユーザーの操作に起因する。 値の指定誤りなど、ユーザーの操作に起 因するエラー。 エラーメッセージの多言語化が必要かも。 インフラストラクチャ レイヤ インターフェース レイヤ アプリケーション レイヤ ドメイン レイヤ

Slide 44

Slide 44 text

エラー処理 2017-09-09 実践ScalaでDDD 44 エラーの変換 利用者にはユーザーフレンドリーなエラー(何が、どう不正で、どうす れば解決できるのか)を返したいが、ドメイン や インフラストラクチャ でユーザーインターフェースを意識すべきではない。 ドメイン や インフラストラクチャ で発生したエラーを、上位レイヤで ユーザーフレンドリーなエラーに変換(読み替え)する必要がある。

Slide 45

Slide 45 text

エラー処理 2017-09-09 実践ScalaでDDD 45 インターフェース アプリケーション サービス リポジトリ イベント パブリッシャ リポジトリ実装 イベント パブリッシャ実装 ドメイン サービス エンティティ ファクトリ クエリ プロセッサ クエリ プロセッサ実装 Javaライブラリ (JDBCドライバ等) 例外 Try Either [ドメインエラー] Either [サービスエラー] Try { … } で囲って、 例外 を Try に変換。 Either[ドメインエラー] および Try を、 Either[サービスエラー] に変換。 Ideas by Yoshitaka Fujii (@yoshiyoshifujii)

Slide 46

Slide 46 text

エラー処理 2017-09-09 実践ScalaでDDD 46 アプリケーションサービスでの変換 アプリケーションサービスで、 - ドメインで発生する Either[ドメインエラー, …] - インフラストラクチャで発生する Try を Either[サービスエラー, …] に変換する。 より自然に変換を書けるように、ヘルパー関数を作っておくとアプリ ケーションサービスのコードが煩雑なエラー処理で汚れない。

Slide 47

Slide 47 text

エラー処理 2017-09-09 実践ScalaでDDD 47 trait TaskService { def assignToTask( … ) = { for { // 値がなかったらサービスエラー task <- taskRepository.get(taskId) ifNotExists NotFoundError // ドメインで異常が発生したらサービスエラー assignedTask <- assignee.assignTo(task) ifLeftThen asServiceError // インフラで失敗したらサービスエラー savedTask <- taskRepository.save(assignedTask.entity) ifFailureThen asServiceError _ <- taskEventPublisher.publish(assignedTask.event) ifFailureThen asServiceError } yield savedTask } }

Slide 48

Slide 48 text

5.コンポーネ ントの実装 Implementing Components 2017-09-09 実践ScalaでDDD 48

Slide 49

Slide 49 text

2017-09-09 実践ScalaでDDD 49 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ 各コンポーネントの実装

Slide 50

Slide 50 text

50 重視すること アプリケーションサービスのコードが、 仕様書(ユースケース記述)になることを目指す。 - アプリケーションサービスが、ドメインのクライアント(利用者)であり、 ドメインの表現の豊かさを映す鏡。 - アプリケーションサービスのコードを自然に読み書きできるように ドメインを作る。

Slide 51

Slide 51 text

2017-09-09 実践ScalaでDDD 51 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ アプリケーションサービス

Slide 52

Slide 52 text

アプリケーションサービス 2017-09-09 実践ScalaでDDD 52 - ドメインレイヤを使って、ユースケースを実現することが責務。 - あくまでもドメインを使うのみ。ドメインの知識がアプリケーション サービスに漏れ出さないようにする。 - インフラストラクチャレイヤには依存しない。 インフラを変更しても、ユースケースを実現するコードはそのまま動 作できる。ユースケースレベルのユニットテストも容易。 - トランザクション境界になる。 実装のポイント

Slide 53

Slide 53 text

アプリケーションサービス 2017-09-09 実践ScalaでDDD 53 - アプリケーションサービス のメソッドの処理は、基本的に ①リポジトリ を使って エンティティ を取得 or エンティティ を生成。 ②エンティティ or ドメインサービス のメソッドで業務の処理。 ③リポジトリ で処理した エンティティ を保存。 ④イベントパブリッシャ で ドメインイベント を発行。 の流れになる。この流れを for内包式 でつなげる。 - ドメインレイヤ、インフラストラクチャレイヤから発生するエラーを、 ユースケースのエラー に変換する。 実装のポイント

Slide 54

Slide 54 text

アプリケーションサービス 2017-09-09 実践ScalaでDDD 54 trait TaskService { val userRepository: UserRepository val taskRepository: TaskRepository val taskEventPublisher: TaskEventPublisher def createNewTask(name: TaskName, authorId: UserId) = tx { implicit unitOfWork => for { author <- userRepository.get(authorId) ifNotExists NotFoundError createdTask = author.createTask(name) savedTask <- taskRepository.save(createdTask.entity) ifFailureThen asServiceError _ <- taskEventPublisher.publish(createdTask.event) ifFailureThen asServiceError } yield savedTask } } コードサンプル

Slide 55

Slide 55 text

2017-09-09 実践ScalaでDDD 55 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ エンティティ

Slide 56

Slide 56 text

エンティティ 2017-09-09 実践ScalaでDDD 56 - 通常、集約 の中で エンティティ は1つ。(集約ルート) - 一意な識別子 エンティティID を持つ。 #equals()、#hashCode() をオーバーライドし、エンティティID でオブ ジェクトの同一性を比較。 - プロパティは バリューオブジェクト で。プリミティブな型は使わない。 - 他集約との関連は、関連先のエンティティIDだけを保持し、 エンティティ そのものを保持しない。 実装のポイント

Slide 57

Slide 57 text

エンティティ 2017-09-09 実践ScalaでDDD 57 - ドメインの振る舞い(ユビキタス言語の動詞句)は、エンティティ のメ ソッドとして実装。 ドメインの振る舞いが アプリケーションサービス に寄ってしまうと、エ ンティティ がただのDTOになってしまう。(ドメインモデル貧血症) - エンティティ のメソッドは、戻り値として 状態が変わった エンティティ と ドメインイベント を返す。 - エンティティ のメソッドとして実装できないような振る舞いは、ドメイン サービス に実装。(あまりないと思う) 実装のポイント

Slide 58

Slide 58 text

エンティティ 2017-09-09 実践ScalaでDDD 58 case class Task( id: TaskId, name: TaskName, state: TaskState, authorId: UserId, comments: Comments ) { def close: Either[TaskAlreadyClosed, DomainResult[Task, TaskClosed] = { val task = copy(state = TaskState.Closed) val event = TaskClosed(task.id) Right(DomainResult(task, event)) } : } コードサンプル

Slide 59

Slide 59 text

2017-09-09 実践ScalaでDDD 59 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ バリューオブジェクト

Slide 60

Slide 60 text

バリューオブジェクト 2017-09-09 実践ScalaでDDD 60 - ドメインの名詞句はすべて バリューオブジェクト にする。 - タイプエイリアス(Ex. type UserName = String)では型安全にならない ので、ちゃんと型を作る。 - scala.AnyVal を継承する。 実行時の バリューオブジェクト の生成コストを抑えることができる。 - オブジェクトの集合は ファーストクラスコレクション を作り、List や Seq、Map などのコレクション型のまま扱わない。 実装のポイント

Slide 61

Slide 61 text

バリューオブジェクト 2017-09-09 実践ScalaでDDD 61 - インターフェース や インフラストラクチャ で頻発する バリューオブ ジェクト と プリミティブ型 との相互変換は、implicit conversion と 型 クラスパターン を使って自動的に変換されるようにすると実装負担 を軽減できる。 実装のポイント

Slide 62

Slide 62 text

バリューオブジェクト 2017-09-09 実践ScalaでDDD 62 trait Value[T] extends Any { def value: T } case class TaskName(value: String) extends AnyVal // バリューオブジェクト sealed abstract class TaskState(val value: String) // 列挙型 object TaskState { case object Opened extends TaskState("OPENED") case object Closed extends TaskState("CLOSED") } case class Comment(value: String) extends AnyVal case class Comments(comments: Seq[Comment]) { // ファーストクラスコレクション def add(comment: Comment) = copy(comments = comments :+ comment) } コードサンプル

Slide 63

Slide 63 text

バリューオブジェクト 2017-09-09 実践ScalaでDDD 63 package object infrastructure { // プリミティブ値 -> バリューオブジェクト に自動変換(型クラスパターン) implicit def wrapValue[P, V <: Value[P]](value: P)(implicit conv: P => V): V = conv(value) // バリューオブジェクト -> プリミティブ値 に自動変換 implicit def unwrapValue[P](value: Value[P]): P = value.value } import infrastructure._ implicit val toTaskName: String => TaskName = (v) => TaskId(v) val taskName: TaskName = "タスク名" // StringがTaskNameに自動変換される val strTaskName: String = taskName // TaskNameがStringに自動変換される コードサンプル

Slide 64

Slide 64 text

2017-09-09 実践ScalaでDDD 64 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ ロールオブジェクト

Slide 65

Slide 65 text

ロールオブジェクト 2017-09-09 実践ScalaでDDD 65 - DDDでは特に定義されていない。(と思う) - 集約 をまたいだ エンティティ と エンティティ の関連をモデル化。 実装のポイント

Slide 66

Slide 66 text

ロールオブジェクト 2017-09-09 実践ScalaでDDD 66 実装のポイント - 関連端(ロール)の名前で型を作る。 - 関連先の エンティティ に手を加えることなく、自集約に特有の振る 舞いを追加できる。 - implicit class で定義することで、エンティティ を自動的に ロールオブ ジェクトに変換。 - ロールオブジェクト を主語として、SVO形式で関連を扱うコードが書 ける。

Slide 67

Slide 67 text

ロールオブジェクト 2017-09-09 実践ScalaでDDD 67 package user { case class User(id: UserId) } package task { case class Task(id: TaskId, assigneeId: UserId) { def assign(assignee: User) = copy(assigneeId = assignee.id) } // 「タスクを担当するユーザー」を表現するロールオブジェクト // Userエンティティを修正することなく、assignTo() と言うメソッドを追加。 implicit class Assignee(user: User) { def assignTo(task: Task): Task = task.assign(user) } } コードサンプル

Slide 68

Slide 68 text

ロールオブジェクト 2017-09-09 実践ScalaでDDD 68 val user: User = … val task: Task = … // ロールオブジェクトを作らない場合 val assignedTask = task.assign(user) // ロールオブジェクトを作った場合 // パッケージを import することで、エンティティがロールオブジェクトに自動変換されるようになる import task._ val assignedTask = user.assignTo(task) コードサンプル

Slide 69

Slide 69 text

2017-09-09 実践ScalaでDDD 69 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ ファクトリ

Slide 70

Slide 70 text

ファクトリ 2017-09-09 実践ScalaでDDD 70 - エンティティ の生成には2パターンある。 ①単独で生成される エンティティ。 ②他の集約の エンティティ から生成される エンティティ。 - 単独で生成される場合は、その エンティティ のコンパニオンオブジェ クトにファクトリメソッドを実装。 - 他の集約の エンティティ から生成される場合は、ロールオブジェクト にファクトリメソッドを実装。 実装のポイント

Slide 71

Slide 71 text

ファクトリ 2017-09-09 実践ScalaでDDD 71 - エンティティIDの生成はインフラストラクチャに依存することが多いの で、ファクトリ にはID生成器を外部から与えるようにしおく。 ユニットテストで、生成されるIDを制御できるのでテストしやすい。 実装のポイント

Slide 72

Slide 72 text

ファクトリ 2017-09-09 実践ScalaでDDD 72 // 単独で生成されるパターン case class User(id: UserId, name: UserName) object User { def create(name: UserName)(implicit idGenerator: EntityIdGenerator) = User(id = UserId(idGenerator.genId), name = name) } val user = User.create(UserName("Taro")) // 他の集約から生成されるパターン case class Task(id: TaskId, name: TaskName, authorId: UserId) implicit class Author(user: User) { def createTask(name: TaskName)(implicit idGenerator: EntityIdGenerator) = Task(id = TaskId(idGenerator.genId), name = name, authorId = user.id) } val task = user.createTask(TaskName("Example task.")) コードサンプル

Slide 73

Slide 73 text

2017-09-09 実践ScalaでDDD 73 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ リポジトリ

Slide 74

Slide 74 text

リポジトリ 2017-09-09 実践ScalaでDDD 74 - エンティティ を、従属する バリューオブジェクト も含めてまるごと永 続化する。エンティティ の一部だけを永続化、とかはない。 - ドメインレイヤ ではインターフェース(トレイト)だけを定義。 実装は インフラストラクチャレイヤ で。 - 通常、以下のメソッドに加えて、いくつかの検索系メソッドを持つ。 ①エンティティID で、エンティティ を1件取得する ②エンティティ を保存する(新規/更新は区別しない) ③エンティティ を削除する(削除があり得ないなら定義しない) 実装のポイント

Slide 75

Slide 75 text

リポジトリ 2017-09-09 実践ScalaでDDD 75 - リポジトリ で扱うのは、自集約の エンティティ のみ。 他集約の エンティティ も含めて扱う(JOINして検索、など)必要が有 る場合は、クエリモデル にする。 - リポジトリ実装では、バリューオブジェクト と プリミティブ値 との相互 変換が多発するので implicit conversion で自動変換すると楽。 実装のポイント

Slide 76

Slide 76 text

リポジトリ 2017-09-09 実践ScalaでDDD 76 package domain.task trait TaskRepository { def get(id: TaskId): Try[Option[Task]] def save(task: Task): Try[Task] def delete(task: Task): Try[Task] } package infrastructure.rdb.task trait TaskRepositoryOnRDB extends TaskRepository { import infrastructure._ // バリューオブジェクトとプリミティブ値の自動変換 import infrastructure.task._ def get(id: TaskId): Try[Option[Task]] = { … } def save(task: Task): Try[Task] = { … } def delete(task: Task): Try[Task] = { … } } コードサンプル

Slide 77

Slide 77 text

2017-09-09 実践ScalaでDDD 77 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ ドメインサービス

Slide 78

Slide 78 text

ドメインサービス 2017-09-09 実践ScalaでDDD 78 - ドメインのビジネスルールを実装した関数の集まり。 - エンティティ のメソッドとしては実装できないような、複雑なドメインの 処理を実装。必要であれば作る、くらいで作る機会はあまりない。 - ファサード ではない。 ドメインサービス からは、リポジトリ や イベントパブリッシャ は使わ ず、副作用を起こさないようにする。 実装のポイント

Slide 79

Slide 79 text

ドメインサービス 2017-09-09 実践ScalaでDDD 79 object TaskService { def applyBusinessRuleTo(task: Task): Task = { … } } val task: Task = … val processedTask = TaskService.applyBusinessRuleTo(task) コードサンプル

Slide 80

Slide 80 text

2017-09-09 実践ScalaでDDD 80 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ ドメインイベント・イベントパブリッシャ

Slide 81

Slide 81 text

ドメインイベント・イベントパブリッシャ 2017-09-09 実践ScalaでDDD 81 - ドメインで起こる「業務上の事象」ごとに、ドメインイベントの型を作る。 (○○が作成された、○○が変更された…など) - CQRS(Command Query Responsibility Segregation)パターンでは、 ドメインイベント を介して、クエリモデル(読み取り専用のビュー)を 非同期に更新する。 - マイクロサービスアーキテクチャでは、サービス同士を非同期に連 携するのに ドメインイベント をやり取りする。 実装のポイント

Slide 82

Slide 82 text

ドメインイベント・イベントパブリッシャ 2017-09-09 実践ScalaでDDD 82 - ドメインイベント では エンティティ を保持しない。 必要な項目だけをプロパティとして持つ。 - ドメインイベント は、エンティティ および ドメインサービス のメソッド で生成し、処理結果といっしょに返す。 - イベントパブリッシャ は、ドメインレイヤ ではインターフェース(トレイ ト)だけを定義。実装は インフラストラクチャレイヤ で。 実装のポイント

Slide 83

Slide 83 text

ドメインイベント・イベントパブリッシャ 2017-09-09 実践ScalaでDDD 83 package domain.task case class TaskCreated( taskId: TaskID, taskName: TaskName, authorId: UserId ) case class DomainResult[ENTITY, EVENT](entity: ENTITY, event: EVENT) class Author(…) { // 戻り値として、生成したエンティティと、エンティティが生成されたことを通知するイベントを返す def createTask(…): DomainResult[Task, TaskCreated] = { val task: Task = … DomainResult(task, TaskCreated(task.id, task.name, task.authorId)) } } コードサンプル

Slide 84

Slide 84 text

ドメインイベント・イベントパブリッシャ 2017-09-09 実践ScalaでDDD 84 trait TaskEventPublisher { def publish(event: TaskCreated): Try[TaskCreated] } package infrastructure.kafka.task trait TaskEventPublisherOnKafka extends TaskEventPublisher { def publish(event: TaskCreated): Try[TaskCreated] = { … } } コードサンプル

Slide 85

Slide 85 text

2017-09-09 実践ScalaでDDD 85 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ クエリモデル・クエリプロセッサ

Slide 86

Slide 86 text

クエリモデル・クエリプロセッサ 2017-09-09 実践ScalaでDDD 86 - クエリモデル には、複数の 集約 の情報をJOINして持たせることがで きる。 - クエリモデル のプロパティに、ドメインの バリューオブジェクト は使 わない。 - クエリプロセッサ には、クエリモデル を取得したり検索するための読 み取りメソッドを定義する。更新系のメソッドは持たない。 - クエリプロセッサ は、クエリレイヤ ではインターフェース(トレイト)だ けを定義。実装は インフラストラクチャレイヤ で。 実装のポイント

Slide 87

Slide 87 text

クエリモデル・クエリプロセッサ 2017-09-09 実践ScalaでDDD 87 package query case class TaskView( taskId: String, taskName: String, authorName: String ) case class TaskSearchResult(hits: Int, items: Seq[TaskView]) trait TaskQueryProcessor { def searchTasks(keyword: String): Try[TaskSearchResult] } package infrastructure.elasticsearch trait TaskQueryProcessorOnES extends TaskQueryProcessor { def searchTasks(keyword: String): Try[TaskSearchResult] = { … } } コードサンプル

Slide 88

Slide 88 text

2017-09-09 実践ScalaでDDD 88 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ イベントサブスクライバ

Slide 89

Slide 89 text

イベントサブスクライバ 2017-09-09 実践ScalaでDDD 89 - ドメインイベント をサブスクライブ(購読)し、クエリモデル を最新化 する。 - コンテキスト をまたぐデータ連携では、必要な他コンテキストの ドメ インイベント をサブスクライブし、自コンテキスト内にそのコピーを作 成・使用することもできる。 - イベントサブスクライバ の実装は、イベントキューとして使用するミド ルウェアやサービスに強く依存する。 実装のポイント

Slide 90

Slide 90 text

2017-09-09 実践ScalaでDDD 90 ファクトリ リポジトリ ドメイン サービス クエリ プロセッサ クエリ モデル イベント パブリッシャ アプリケーション サービス インターフェース エンティティ バリュー オブジェクト ロール オブジェクト ドメイン イベント イベント サブスクライバ インターフェース

Slide 91

Slide 91 text

インターフェース 2017-09-09 実践ScalaでDDD 91 - アプリケーションサービス を呼び出して、ユースケースを起動する。 - アプリケーション/クエリ/ドメイン に、使用するインフラストラクチャ を選択して合成し、実行可能なオブジェクトを作る。 - 実装は採用するフレームワーク(Play Framework、Akka HTTP…etc) による。 実装のポイント

Slide 92

Slide 92 text

インターフェース 2017-09-09 実践ScalaでDDD 92 trait TaskController { val taskService: TaskService def example = { taskService.createNewTask(TaskName("Example", UserId("TEST")) } } class TaskControllerImpl extends TaskController { // 使用するコンポーネントに、インフラストラクチャの実装を合成 override val taskService = new TaskService with TransactionAware { override val userRepository = new UserRepository with UserRepositoryOnRDB override val taskRepository = new TaskRepository with TaskRepositoryOnRDB override val taskEventPublisher = new TaskEventPublisher with TaskEventPublisherOnKafka } } コードサンプル

Slide 93

Slide 93 text

2017-09-09 実践ScalaでDDD 93 実装例は、GitHubのサンプルコードを参照してください。 https://github.com/crossroad0201/ddd-on-scala

Slide 94

Slide 94 text

Practicing DDD 6.DDD の実践 2017-09-09 実践ScalaでDDD 94

Slide 95

Slide 95 text

さらにこの先にあるもの… 2017-09-09 実践ScalaでDDD 95 以下のようなテクノロジを統合していく。 - イベントソーシング ドメインオブジェクトの永続化で、データをUPDATEするのではなく、 イベントをINSERTしていくことで状態を保存。 取得時は蓄積されているイベントを順番に再適用(リプレイ)するこ とで、ドメインオブジェクトの状態を復元する。 - アクターモデル Akka Stream を使ったアクターモデルによるプログラミング。

Slide 96

Slide 96 text

継続的な学習が必要 2017-09-09 実践ScalaでDDD 96 - DDDに正解はない。 - 実践して、学び、改良して自分たちなりのスタイルを作っていく。 - 採用するテクノロジは、自分たちのスキルセットと相談して… - マイクロサービスアーキテクチャ なら、コンテキストごとにサービス を分割して開発していくので、新しい学びを段階的に導入しやすい。

Slide 97

Slide 97 text

2017-09-09 実践ScalaでDDD 97 at Mt.Fuji 2016