Save 37% off PRO during our Black Friday Sale! »

ドメインをモデリングしてPHPコードに落とし込む / domain-modeling-with-php8

ドメインをモデリングしてPHPコードに落とし込む / domain-modeling-with-php8

PHP カンファレンス 2021 / フィードバックはこちらへ https://joind.in/talk/650b0

Ca17a082a30f4cbfed1d0a6dacbe3af2?s=128

shin1x1
PRO

October 02, 2021
Tweet

Transcript

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

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

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

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

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

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

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

  8. 共通概念を作ろう! 8

  9. 共通概念を作ろう! 9

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

    10
  11. 今日のドメイン 11

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

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

  14. ワクチン接種の流れ 14

  15. モデリング 15

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

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

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

  19. 用語集例 用語 英語表記 内容 接種者 recipient ワクチン接種を受ける人。 予約登録 reserve 接種者がワクチン接種を予約する行為。

    未予約の接種者のみ予約できる。 予約接種日 reserved date ワクチン接種の日(年月日)。 予約登録にて接種者が指定する。 予約登録を行う日から7日以降、30日以内。 19
  20. 概念モデル(ドメインモデル)図の作成 用語集の用語を簡易クラス図として並べる。 各用語の関連を線で結んだり、グルーピングするなどして整理する。 用語を俯瞰で見て違和感が無いか、漏れが無いかを見る。 20

  21. 概念モデル図例 21

  22. コードに実装して検証 22

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

  24. ドメインモデル実装 1 モデル = 1 クラス。 クラスにすることで型検査の恩恵を受けられる POPO(Plain Old PHP

    Object) で実装。 クラスやメソッドの名前にドメインモデルの用語を使う。 モデルの制約をクラスの実装に閉じ込める。 イミュータブルオブジェクトにする。 setter メソッドを作らない。 ドメインロジックによってプロパティの値を変える。 (ex. set予約() ではなく、予約登録() にする。) 24
  25. 接種券番号クラス 接種券番号の制約(数字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
  26. 接種券番号クラスのテスト POPO なのでテストが容易。 制約に違反していれば例外がスローされることを確認。 /** * @test */ public function

    construct_ 数字以外ならエラー(): void { $this->expectException(InvariantException::class); new 接種券番号('A234567890'); } 26
  27. 予約接種日クラス ファクリメソッドで予約接種日を示す文字列と現在日を受け取り、ドメインルール を検証する。(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
  28. 接種者Idクラス 接種者の識別子を示す。 識別子はメソッドの引数になることも多く、型検査の恩恵も大きい。 プロジェクト内では他の ID も同様の実装になることが多いので、trait 等で共通化す ると実装が楽。 final class

    接種者Id { use SequencialId; } 28
  29. 接種者クラス 接種者ID、予約、接種をプロパティに持つ。 それぞれの値の制約はそれぞれのクラスで実装。 予約、接種は値が無い場合があるので nullable にしている。 final class 接種者 {

    public function __construct( private 接種者Id $id, private ? 予約 $ 予約 = null, private ? 接種 $ 接種 = null, ) { } 29
  30. 接種者クラス - 予約登録メソッド 予約登録というドメインロジックをメソッドに実装。 既に予約がある場合は予約完了とみなして例外をスロー。 予約を含む新しいインスタンスを生成して返す。 public function 予約登録( 予約

    $ 予約): self { if ($this-> 予約 !== null) { throw new PreconditionException(); } return new self( $this->id, $ 予約, ); } 30
  31. 予約登録メソッドのテスト 予約登録メソッドで生成した新しいインスタンスに予約が含まれているか確認。 /** * @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. モデリングの一環としての実装 モデリングしてコードを実装することでより理解深まる。 曖昧さを排除するのでモデルの不整合や不備を発見できる。 実際、コードに実装して気づくことが多い(ですよね?)。 テストで実行して検証できる。 コードを書ける人がモデリングする利点。 32

  33. ユースケースで検証 33

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

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

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

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

  38. ユースケースシナリオ実装 1 ユースケースシナリオ = 1 クラス。 public メソッドはシナリオ実行の 1 つだけ。

    ドメインに関する処理はドメインオブジェクトで実装。 POPO で実装すると責務が限定でき、テストも容易。 データベースなど IO に関する処理はインターフェイスで抽象化。 38
  39. 参考: インターフェイスで IO を抽象化する例 https://speakerdeck.com/shin1x1/independent-core-layer-pattern-phpconsen2019 39

  40. 予約登録ユースケースクラスの実装 ドメインオブジェクトを生成、取得して、ドメインロジックを実行。 ドメインロジックで更新したオブジェクトをデータベースに保存。 final class 予約登録UseCase { public function run(

    接種券番号 $ 接種券番号, 自治体番号 $ 自治体番号, 予約接種日 $ 予約接種日): void { // データベースからドメインオブジェクトを取得 $ 接種者 = $this->query->find($ 接種券番号, $ 自治体番号); if ($ 接種者 === null) { throw new PreconditionException(' 該当する接種者が存在しません'); } // ドメインロジックを実行 $ 接種者 = $ 接種者-> 予約登録(new 予約($date)); // 結果ドメインオブジェクトをデータベースに保存 $this->command->store($ 接種者); } } 40
  41. モデル、コードの改善 41

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

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

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

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

    未予約; case 予約完了; case 接種完了; } 45
  46. 接種者クラスの変更 コンストラクタで接種ステータスを追加。初期値は未予約とする。 final class 接種者 { public function __constructor( private

    接種者Id $id, private 接種ステータス $ 接種ステータス = 接種ステータス:: 未予約, private ? 予約 $ 予約 = null, ) {} 46
  47. 接種者クラスの変更 - 予約登録メソッド 予約登録の事前条件検証を接種ステータスを見て行う。 予約キャンセル、接種登録メソッドも同様に変更。 public function 予約登録( 予約 $

    予約): self { if ($this-> 接種ステータス !== 接種ステータス:: 未予約) { throw new InvalidOperationException(' 現在のステータスで予約できません'); } return new self( $this->id, 接種ステータス:: 予約完了, $ 予約, ); } 47
  48. 予約ユースケースは変更なし ドメインクラスの変更のみなので、ユースケースクラスは変更無し。 final class 予約登録UseCase { public function run( 接種券番号

    $ 接種券番号, 自治体番号 $ 自治体番号, 予約接種日 $date, ): void { $ 接種者 = $this->query->findBy 接種券番号And 自治体番号($ 接種券番号, $ 自治体番号); if ($ 接種者 === null) { throw new PreconditionException(' 該当する接種者が存在しません'); } $ 接種者 = $ 接種者-> 予約登録(new 予約($date)); $this->command->store($ 接種者); } } 48
  49. まとめ 49

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

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

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

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

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

    54
  55. 参照 エリック・エヴァンスのドメイン駆動設計 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