Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

shin1x1
October 02, 2021

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

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

shin1x1

October 02, 2021
Tweet

More Decks by shin1x1

Other Decks in Programming

Transcript

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

    未予約の接種者のみ予約できる。 予約接種日 reserved date ワクチン接種の日(年月日)。 予約登録にて接種者が指定する。 予約登録を行う日から7日以降、30日以内。 19
  2. ドメインモデル実装 1 モデル = 1 クラス。 クラスにすることで型検査の恩恵を受けられる POPO(Plain Old PHP

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

    construct_ 数字以外ならエラー(): void { $this->expectException(InvariantException::class); new 接種券番号('A234567890'); } 26
  5. 予約接種日クラス ファクリメソッドで予約接種日を示す文字列と現在日を受け取り、ドメインルール を検証する。(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
  6. 予約登録メソッドのテスト 予約登録メソッドで生成した新しいインスタンスに予約が含まれているか確認。 /** * @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
  7. ユースケースシナリオ実装 1 ユースケースシナリオ = 1 クラス。 public メソッドはシナリオ実行の 1 つだけ。

    ドメインに関する処理はドメインオブジェクトで実装。 POPO で実装すると責務が限定でき、テストも容易。 データベースなど IO に関する処理はインターフェイスで抽象化。 38
  8. 予約登録ユースケースクラスの実装 ドメインオブジェクトを生成、取得して、ドメインロジックを実行。 ドメインロジックで更新したオブジェクトをデータベースに保存。 final class 予約登録UseCase { public function run(

    接種券番号 $ 接種券番号, 自治体番号 $ 自治体番号, 予約接種日 $ 予約接種日): void { // データベースからドメインオブジェクトを取得 $ 接種者 = $this->query->find($ 接種券番号, $ 自治体番号); if ($ 接種者 === null) { throw new PreconditionException(' 該当する接種者が存在しません'); } // ドメインロジックを実行 $ 接種者 = $ 接種者-> 予約登録(new 予約($date)); // 結果ドメインオブジェクトをデータベースに保存 $this->command->store($ 接種者); } } 40
  9. 接種者クラスの変更 コンストラクタで接種ステータスを追加。初期値は未予約とする。 final class 接種者 { public function __constructor( private

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

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

    $ 接種券番号, 自治体番号 $ 自治体番号, 予約接種日 $date, ): void { $ 接種者 = $this->query->findBy 接種券番号And 自治体番号($ 接種券番号, $ 自治体番号); if ($ 接種者 === null) { throw new PreconditionException(' 該当する接種者が存在しません'); } $ 接種者 = $ 接種者-> 予約登録(new 予約($date)); $this->command->store($ 接種者); } } 48
  12. 参照 エリック・エヴァンスのドメイン駆動設計 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