PHP カンファレンス 2021 / フィードバックはこちらへ https://joind.in/talk/650b0
ドメインをモデリングしてPHPコードに落とし込む2021/10/02 phpcon 2021 @shin1x1
View Slide
@shin1x1新原(しんばら) 雅司1×1株式会社Web アプリケーション開発技術サポートPHP の現場https://php-genba.shin1x1.com/
サンプルコードhttps://github.com/shin1x1/domain-modeling-with-php
業務システムでのミスコミュニケーション4
例. 同じ言葉なのに概念が違う同じ概念を違う言葉で表現もあり。5
例. 概念が途中で抜け落ちる6
例. コードから概念を想起できない7
共通概念を作ろう!8
共通概念を作ろう!9
ドメインモデルドメイン: ソフトウェアが扱う対象領域。業務アプリケーションであれば、対象業務がドメイン。モデル: 対象から目的に必要な要素を抽出した概念。ドメインモデルドメインから必要な要素を抽出した概念。必要な概念を作り出したり、変更することもある。通常システムの技術要素は含まない。(技術要素がドメインの場合を除く)10
今日のドメイン11
本発表のドメインは架空のものであり実際のものとは関係ありません12
ワクチン接種システムワクチン接種は事前予約の上、接種会場にて行う。ワクチン接種は現在は 1 回のみ。接種者のワクチン接種予約、接種登録を行うシステムを構築する。13
ワクチン接種の流れ14
モデリング15
モデリングシステム化に必要なドメインの要素を抽出。複数の視点、手法で徐々に形にしていく。マクロ視点概念モデル図、ユースケース図ミクロ視点用語集、ユースケースシナリオ、コード業務チームからのフィードバックを得て精度を高めていく。16
ユースケース図ユーザとシステム提供機能の関係を示す。システム化範囲(システムに含めるもの、含めないもの)を示す。自治体や医師は本システムのユーザではない。17
用語集の作成業務や要求の資料、ヒアリングなどからドメインの用語を表にまとめる。用語(英語表記)、意味、制約など。同じ概念には同じ名前を付ける。意思疎通するために大事。別名がある場合はそれも記載。名詞だけでなく、アクションやイベントも含める。18
用語集例用語 英語表記 内容接種者 recipient ワクチン接種を受ける人。予約登録 reserve 接種者がワクチン接種を予約する行為。未予約の接種者のみ予約できる。予約接種日 reserved dateワクチン接種の日(年月日)。予約登録にて接種者が指定する。予約登録を行う日から7日以降、30日以内。19
概念モデル(ドメインモデル)図の作成用語集の用語を簡易クラス図として並べる。各用語の関連を線で結んだり、グルーピングするなどして整理する。用語を俯瞰で見て違和感が無いか、漏れが無いかを見る。20
概念モデル図例21
コードに実装して検証22
コードに実装して検証モデリングの一環としてコードに実装。あくまでスケッチなので細かな処理は後回しで良い。23
ドメインモデル実装1 モデル = 1 クラス。クラスにすることで型検査の恩恵を受けられるPOPO(Plain Old PHP Object) で実装。クラスやメソッドの名前にドメインモデルの用語を使う。モデルの制約をクラスの実装に閉じ込める。イミュータブルオブジェクトにする。setter メソッドを作らない。ドメインロジックによってプロパティの値を変える。(ex. set予約() ではなく、予約登録() にする。)24
接種券番号クラス接種券番号の制約(数字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
接種券番号クラスのテストPOPO なのでテストが容易。制約に違反していれば例外がスローされることを確認。/*** @test*/public function construct_数字以外ならエラー(): void{$this->expectException(InvariantException::class);new接種券番号('A234567890');}26
予約接種日クラスファクリメソッドで予約接種日を示す文字列と現在日を受け取り、ドメインルールを検証する。(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
接種者Idクラス接種者の識別子を示す。識別子はメソッドの引数になることも多く、型検査の恩恵も大きい。プロジェクト内では他の ID も同様の実装になることが多いので、trait 等で共通化すると実装が楽。final class接種者Id{use SequencialId;}28
接種者クラス接種者ID、予約、接種をプロパティに持つ。それぞれの値の制約はそれぞれのクラスで実装。予約、接種は値が無い場合があるので nullable にしている。final class接種者{public function __construct(private接種者Id $id,private ?予約 $予約 = null,private ?接種 $接種 = null,) {}29
接種者クラス - 予約登録メソッド予約登録というドメインロジックをメソッドに実装。既に予約がある場合は予約完了とみなして例外をスロー。予約を含む新しいインスタンスを生成して返す。public function予約登録(予約 $予約): self{if ($this->予約 !== null) {throw new PreconditionException();}return new self($this->id,$予約,);}30
予約登録メソッドのテスト予約登録メソッドで生成した新しいインスタンスに予約が含まれているか確認。/*** @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
モデリングの一環としての実装モデリングしてコードを実装することでより理解深まる。曖昧さを排除するのでモデルの不整合や不備を発見できる。実際、コードに実装して気づくことが多い(ですよね?)。テストで実行して検証できる。コードを書ける人がモデリングする利点。32
ユースケースで検証33
ユースケースでモデルを検証ユースケースシナリオでドメインモデルを利用して検証。ユースケース記述ユースケースのシナリオを文章で記述。34
ユースケース記述例 - 予約登録主なアクター接種者事前条件(シナリオ実行前に満たすべき条件)接種者は接種券を持っている。接種者は予約も接種も完了していない。事後条件(シナリオ実行後に満たすべき条件)接種者の予約が登録される。35
基本フロー(正常系)接種者は、予約画面にアクセスする。接種者は、接種券の接種券番号と自治体番号、予約接種日を入力、送信する。システムは、接種券番号と自治体番号から接種者を特定する。システムは、接種者の予約登録を行う。36
代替フロー(基本フロー以外、異常系)以下の場合は入力エラーとする。接種券番号、自治体番号に該当する接種者が存在しない。該当する接種者が既に予約もしくは接種を完了している。接種日が現在日から 7 日以降、30 日以内の範囲を超えている。37
ユースケースシナリオ実装1 ユースケースシナリオ = 1 クラス。public メソッドはシナリオ実行の 1 つだけ。ドメインに関する処理はドメインオブジェクトで実装。POPO で実装すると責務が限定でき、テストも容易。データベースなど IO に関する処理はインターフェイスで抽象化。38
参考: インターフェイスで IO を抽象化する例https://speakerdeck.com/shin1x1/independent-core-layer-pattern-phpconsen201939
予約登録ユースケースクラスの実装ドメインオブジェクトを生成、取得して、ドメインロジックを実行。ドメインロジックで更新したオブジェクトをデータベースに保存。final class予約登録UseCase{public function run(接種券番号 $接種券番号,自治体番号 $自治体番号,予約接種日 $予約接種日): void{//データベースからドメインオブジェクトを取得$接種者 = $this->query->find($接種券番号, $自治体番号);if ($接種者 === null) {throw new PreconditionException('該当する接種者が存在しません');}//ドメインロジックを実行$接種者 = $接種者->予約登録(new予約($date));//結果ドメインオブジェクトをデータベースに保存$this->command->store($接種者);}}40
モデル、コードの改善41
接種状況の判定予約済や接種済といった接種状況の判定を予約や接種プロパティの値で行っている。予約に値が無く、接種に値があるという不正な状態が起こりうる。接種状況のバリエーションが増えると遷移のパターンも増えていく。接種状況を明確に示す接種ステータスを追加。42
概念モデル図ドメインモデルに接種ステータスを追加43
接種ステータスの状態遷移状態遷移のルールをアクティビティ図で示して、遷移パターンを明示。図で示されていない遷移はエラーとなる。44
接種ステータスの実装enum(PHP 8.1 で導入)で状態を表現。enum は型として扱える。enum接種ステータス{case未予約;case予約完了;case接種完了;}45
接種者クラスの変更コンストラクタで接種ステータスを追加。初期値は未予約とする。final class接種者{public function __constructor(private接種者Id $id,private接種ステータス $接種ステータス =接種ステータス::未予約,private ?予約 $予約 = null,) {}46
接種者クラスの変更 - 予約登録メソッド予約登録の事前条件検証を接種ステータスを見て行う。予約キャンセル、接種登録メソッドも同様に変更。public function予約登録(予約 $予約): self{if ($this->接種ステータス !==接種ステータス::未予約) {throw new InvalidOperationException('現在のステータスで予約できません');}return new self($this->id,接種ステータス::予約完了,$予約,);}47
予約ユースケースは変更なしドメインクラスの変更のみなので、ユースケースクラスは変更無し。final class予約登録UseCase{public function run(接種券番号 $接種券番号,自治体番号 $自治体番号,予約接種日 $date,): void {$接種者 = $this->query->findBy接種券番号And自治体番号($接種券番号, $自治体番号);if ($接種者 === null) {throw new PreconditionException('該当する接種者が存在しません');}$接種者 = $接種者->予約登録(new予約($date));$this->command->store($接種者);}}48
まとめ49
まとめドメインから必要な要素を抽出してドメインモデルを構築。ドメインモデルを共通概念として共有。ドメインモデルの語彙や知識をドメインクラスに実装。コードが書けるからこそ、モデリングをやってみよう!50
フィードバックお待ちしてます!https://joind.in/talk/650b051
想定 FAQ1 - どこからやるのが良い?まずは用語集から。用語集を作って認識合わせするだけでかなり理解が進む。後からプロジェクトに入る人も嬉しい。あとユースケース図かな。52
想定 FAQ2 - 図やドキュメントのメンテナンスは?もちろんやるのが理想、ではある。コミュニケーションツールと割り切って、Wiki 等にスナップショットとして残す。PlantUML 使うとレイアウト調整を諦められる :)53
想定 FAQ3 - ドメインクラスのプロパティは全てクラス定義?ドメインモデルの実装もシステム全体で見れば部品なので必要に応じて実装。ドメインの制約やロジックがある。識別子のようにあえてロジックが無いもので示したい。型検査したい場合。そうでなければスカラー型でも良い。54
参照エリック・エヴァンスのドメイン駆動設計https://www.amazon.co.jp/dp/4798121967Domain 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-ndahua55