Slide 1

Slide 1 text

ドメインをモデリングしてPHPコードに落とし込む 2021/10/02 phpcon 2021 @shin1x1

Slide 2

Slide 2 text

@shin1x1 新原(しんばら) 雅司 1×1株式会社 Web アプリケーション開発 技術サポート PHP の現場 https://php-genba.shin1x1.com/

Slide 3

Slide 3 text

サンプルコード https://github.com/shin1x1/domain-modeling-with-php

Slide 4

Slide 4 text

業務システムでのミスコミュニケーション 4

Slide 5

Slide 5 text

例. 同じ言葉なのに概念が違う 同じ概念を違う言葉で表現もあり。 5

Slide 6

Slide 6 text

例. 概念が途中で抜け落ちる 6

Slide 7

Slide 7 text

例. コードから概念を想起できない 7

Slide 8

Slide 8 text

共通概念を作ろう! 8

Slide 9

Slide 9 text

共通概念を作ろう! 9

Slide 10

Slide 10 text

ドメインモデル ドメイン: ソフトウェアが扱う対象領域。業務アプリケーションであれば、対象業務 がドメイン。 モデル: 対象から目的に必要な要素を抽出した概念。 ドメインモデル ドメインから必要な要素を抽出した概念。 必要な概念を作り出したり、変更することもある。 通常システムの技術要素は含まない。(技術要素がドメインの場合を除く) 10

Slide 11

Slide 11 text

今日のドメイン 11

Slide 12

Slide 12 text

本発表のドメインは架空のものであり 実際のものとは関係ありません 12

Slide 13

Slide 13 text

ワクチン接種システム ワクチン接種は事前予約の上、接種会場にて行う。 ワクチン接種は現在は 1 回のみ。 接種者のワクチン接種予約、接種登録を行うシステムを構築する。 13

Slide 14

Slide 14 text

ワクチン接種の流れ 14

Slide 15

Slide 15 text

モデリング 15

Slide 16

Slide 16 text

モデリング システム化に必要なドメインの要素を抽出。 複数の視点、手法で徐々に形にしていく。 マクロ視点 概念モデル図、ユースケース図 ミクロ視点 用語集、ユースケースシナリオ、コード 業務チームからのフィードバックを得て精度を高めていく。 16

Slide 17

Slide 17 text

ユースケース図 ユーザとシステム提供機能の関係を示す。 システム化範囲(システムに含めるもの、含めないもの)を示す。 自治体や医師は本システムのユーザではない。 17

Slide 18

Slide 18 text

用語集の作成 業務や要求の資料、ヒアリングなどからドメインの用語を表にまとめる。 用語(英語表記)、意味、制約など。 同じ概念には同じ名前を付ける。 意思疎通するために大事。 別名がある場合はそれも記載。 名詞だけでなく、アクションやイベントも含める。 18

Slide 19

Slide 19 text

用語集例 用語 英語表記 内容 接種者 recipient ワクチン接種を受ける人。 予約登録 reserve 接種者がワクチン接種を予約する行為。 未予約の接種者のみ予約できる。 予約接種日 reserved date ワクチン接種の日(年月日)。 予約登録にて接種者が指定する。 予約登録を行う日から7日以降、30日以内。 19

Slide 20

Slide 20 text

概念モデル(ドメインモデル)図の作成 用語集の用語を簡易クラス図として並べる。 各用語の関連を線で結んだり、グルーピングするなどして整理する。 用語を俯瞰で見て違和感が無いか、漏れが無いかを見る。 20

Slide 21

Slide 21 text

概念モデル図例 21

Slide 22

Slide 22 text

コードに実装して検証 22

Slide 23

Slide 23 text

コードに実装して検証 モデリングの一環としてコードに実装。 あくまでスケッチなので細かな処理は後回しで良い。 23

Slide 24

Slide 24 text

ドメインモデル実装 1 モデル = 1 クラス。 クラスにすることで型検査の恩恵を受けられる POPO(Plain Old PHP Object) で実装。 クラスやメソッドの名前にドメインモデルの用語を使う。 モデルの制約をクラスの実装に閉じ込める。 イミュータブルオブジェクトにする。 setter メソッドを作らない。 ドメインロジックによってプロパティの値を変える。 (ex. set予約() ではなく、予約登録() にする。) 24

Slide 25

Slide 25 text

接種券番号クラス 接種券番号の制約(数字10桁)をコンストラクタで実装。 インスタンス化 = 制約を満たすことになる。 final class 接種券番号 { public function __construct(private string $code) { if (preg_match('/\A[0-9]{10}\z/', $code) !== 1) { throw new InvariantException('Invalid code:' . $code); } } } 25

Slide 26

Slide 26 text

接種券番号クラスのテスト POPO なのでテストが容易。 制約に違反していれば例外がスローされることを確認。 /** * @test */ public function construct_ 数字以外ならエラー(): void { $this->expectException(InvariantException::class); new 接種券番号('A234567890'); } 26

Slide 27

Slide 27 text

予約接種日クラス ファクリメソッドで予約接種日を示す文字列と現在日を受け取り、ドメインルール を検証する。(7日以降、30日以内) final class 予約接種日 { public function __construct(private Date $date) { } public static function createFromString(string $dateString, Date $now): self { $date = Date::createFromString($dateString); // TODO: $now の 7 日以降 30 日以内でなければ例外をスロー return new self($date); } } 27

Slide 28

Slide 28 text

接種者Idクラス 接種者の識別子を示す。 識別子はメソッドの引数になることも多く、型検査の恩恵も大きい。 プロジェクト内では他の ID も同様の実装になることが多いので、trait 等で共通化す ると実装が楽。 final class 接種者Id { use SequencialId; } 28

Slide 29

Slide 29 text

接種者クラス 接種者ID、予約、接種をプロパティに持つ。 それぞれの値の制約はそれぞれのクラスで実装。 予約、接種は値が無い場合があるので nullable にしている。 final class 接種者 { public function __construct( private 接種者Id $id, private ? 予約 $ 予約 = null, private ? 接種 $ 接種 = null, ) { } 29

Slide 30

Slide 30 text

接種者クラス - 予約登録メソッド 予約登録というドメインロジックをメソッドに実装。 既に予約がある場合は予約完了とみなして例外をスロー。 予約を含む新しいインスタンスを生成して返す。 public function 予約登録( 予約 $ 予約): self { if ($this-> 予約 !== null) { throw new PreconditionException(); } return new self( $this->id, $ 予約, ); } 30

Slide 31

Slide 31 text

予約登録メソッドのテスト 予約登録メソッドで生成した新しいインスタンスに予約が含まれているか確認。 /** * @test */ public function 予約登録() { $sut = new 接種者(new 接種者Id()); $reservation = new 予約( new 予約接種日(Date::createFromString('2021-09-19'))); $actual = $sut-> 予約($reservation); $expected = new 接種者(new 接種者Id(), 予約: $reservation, 接種: null); $this->assertEquals($expected, $actual); } 31

Slide 32

Slide 32 text

モデリングの一環としての実装 モデリングしてコードを実装することでより理解深まる。 曖昧さを排除するのでモデルの不整合や不備を発見できる。 実際、コードに実装して気づくことが多い(ですよね?)。 テストで実行して検証できる。 コードを書ける人がモデリングする利点。 32

Slide 33

Slide 33 text

ユースケースで検証 33

Slide 34

Slide 34 text

ユースケースでモデルを検証 ユースケースシナリオでドメインモデルを利用して検証。 ユースケース記述 ユースケースのシナリオを文章で記述。 34

Slide 35

Slide 35 text

ユースケース記述例 - 予約登録 主なアクター 接種者 事前条件(シナリオ実行前に満たすべき条件) 接種者は接種券を持っている。 接種者は予約も接種も完了していない。 事後条件(シナリオ実行後に満たすべき条件) 接種者の予約が登録される。 35

Slide 36

Slide 36 text

基本フロー(正常系) 接種者は、予約画面にアクセスする。 接種者は、接種券の接種券番号と自治体番号、予約接種日を入力、送信する。 システムは、接種券番号と自治体番号から接種者を特定する。 システムは、接種者の予約登録を行う。 36

Slide 37

Slide 37 text

代替フロー(基本フロー以外、異常系) 以下の場合は入力エラーとする。 接種券番号、自治体番号に該当する接種者が存在しない。 該当する接種者が既に予約もしくは接種を完了している。 接種日が現在日から 7 日以降、30 日以内の範囲を超えている。 37

Slide 38

Slide 38 text

ユースケースシナリオ実装 1 ユースケースシナリオ = 1 クラス。 public メソッドはシナリオ実行の 1 つだけ。 ドメインに関する処理はドメインオブジェクトで実装。 POPO で実装すると責務が限定でき、テストも容易。 データベースなど IO に関する処理はインターフェイスで抽象化。 38

Slide 39

Slide 39 text

参考: インターフェイスで IO を抽象化する例 https://speakerdeck.com/shin1x1/independent-core-layer-pattern-phpconsen2019 39

Slide 40

Slide 40 text

予約登録ユースケースクラスの実装 ドメインオブジェクトを生成、取得して、ドメインロジックを実行。 ドメインロジックで更新したオブジェクトをデータベースに保存。 final class 予約登録UseCase { public function run( 接種券番号 $ 接種券番号, 自治体番号 $ 自治体番号, 予約接種日 $ 予約接種日): void { // データベースからドメインオブジェクトを取得 $ 接種者 = $this->query->find($ 接種券番号, $ 自治体番号); if ($ 接種者 === null) { throw new PreconditionException(' 該当する接種者が存在しません'); } // ドメインロジックを実行 $ 接種者 = $ 接種者-> 予約登録(new 予約($date)); // 結果ドメインオブジェクトをデータベースに保存 $this->command->store($ 接種者); } } 40

Slide 41

Slide 41 text

モデル、コードの改善 41

Slide 42

Slide 42 text

接種状況の判定 予約済や接種済といった接種状況の判定を予約や接種プロパティの値で行ってい る。 予約に値が無く、接種に値があるという不正な状態が起こりうる。 接種状況のバリエーションが増えると遷移のパターンも増えていく。 接種状況を明確に示す接種ステータスを追加。 42

Slide 43

Slide 43 text

概念モデル図 ドメインモデルに接種ステータスを追加 43

Slide 44

Slide 44 text

接種ステータスの状態遷移 状態遷移のルールをアクティビティ図で示して、遷移パターンを明示。 図で示されていない遷移はエラーとなる。 44

Slide 45

Slide 45 text

接種ステータスの実装 enum(PHP 8.1 で導入)で状態を表現。 enum は型として扱える。 enum 接種ステータス { case 未予約; case 予約完了; case 接種完了; } 45

Slide 46

Slide 46 text

接種者クラスの変更 コンストラクタで接種ステータスを追加。初期値は未予約とする。 final class 接種者 { public function __constructor( private 接種者Id $id, private 接種ステータス $ 接種ステータス = 接種ステータス:: 未予約, private ? 予約 $ 予約 = null, ) {} 46

Slide 47

Slide 47 text

接種者クラスの変更 - 予約登録メソッド 予約登録の事前条件検証を接種ステータスを見て行う。 予約キャンセル、接種登録メソッドも同様に変更。 public function 予約登録( 予約 $ 予約): self { if ($this-> 接種ステータス !== 接種ステータス:: 未予約) { throw new InvalidOperationException(' 現在のステータスで予約できません'); } return new self( $this->id, 接種ステータス:: 予約完了, $ 予約, ); } 47

Slide 48

Slide 48 text

予約ユースケースは変更なし ドメインクラスの変更のみなので、ユースケースクラスは変更無し。 final class 予約登録UseCase { public function run( 接種券番号 $ 接種券番号, 自治体番号 $ 自治体番号, 予約接種日 $date, ): void { $ 接種者 = $this->query->findBy 接種券番号And 自治体番号($ 接種券番号, $ 自治体番号); if ($ 接種者 === null) { throw new PreconditionException(' 該当する接種者が存在しません'); } $ 接種者 = $ 接種者-> 予約登録(new 予約($date)); $this->command->store($ 接種者); } } 48

Slide 49

Slide 49 text

まとめ 49

Slide 50

Slide 50 text

まとめ ドメインから必要な要素を抽出してドメインモデルを構築。 ドメインモデルを共通概念として共有。 ドメインモデルの語彙や知識をドメインクラスに実装。 コードが書けるからこそ、モデリングをやってみよう! 50

Slide 51

Slide 51 text

フィードバックお待ちしてます! https://joind.in/talk/650b0 51

Slide 52

Slide 52 text

想定 FAQ1 - どこからやるのが良い? まずは用語集から。 用語集を作って認識合わせするだけでかなり理解が進む。 後からプロジェクトに入る人も嬉しい。 あとユースケース図かな。 52

Slide 53

Slide 53 text

想定 FAQ2 - 図やドキュメントのメンテナンスは? もちろんやるのが理想、ではある。 コミュニケーションツールと割り切って、Wiki 等にスナップショットとして残す。 PlantUML 使うとレイアウト調整を諦められる :) 53

Slide 54

Slide 54 text

想定 FAQ3 - ドメインクラスのプロパティは全てクラス 定義? ドメインモデルの実装もシステム全体で見れば部品なので必要に応じて実装。 ドメインの制約やロジックがある。 識別子のようにあえてロジックが無いもので示したい。 型検査したい場合。 そうでなければスカラー型でも良い。 54

Slide 55

Slide 55 text

参照 エリック・エヴァンスのドメイン駆動設計 https://www.amazon.co.jp/dp/4798121967 Domain Modeling Made Functional: Tackle Software Complexity with Domain- Driven Design and F# https://www.amazon.co.jp/dp/1680502549 オブジェクト指向モデル http://www.ics.kagoshima-u.ac.jp/edu/SoftwareEngineering/oo-model.html モデリングで既存システムの可視化に臨んだ話 https://speakerdeck.com/jnuank/moderingudeji-cun-sisutemufalseke-shi-hua- nilin-ndahua 55