$30 off During Our Annual Pro Sale. View Details »

ドメインをモデリングして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. ドメインをモデリングしてPHPコードに落とし込む
    2021/10/02 phpcon 2021 @shin1x1

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  11. 今日のドメイン
    11

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. モデリング
    15

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. 概念モデル図例
    21

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  26. 接種券番号クラスのテスト
    POPO なのでテストが容易。
    制約に違反していれば例外がスローされることを確認。
    /**

    * @test

    */

    public function construct_
    数字以外ならエラー(): void

    {

    $this->expectException(InvariantException::class);

    new
    接種券番号('A234567890');

    }

    26

    View Slide

  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

    View Slide

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

    {

    use SequencialId;

    }

    28

    View Slide

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

    {

    public function __construct(

    private
    接種者Id $id,

    private ?
    予約 $
    予約 = null,

    private ?
    接種 $
    接種 = null,

    ) {

    }

    29

    View Slide

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

    {

    if ($this->
    予約 !== null) {

    throw new PreconditionException();

    }

    return new self(

    $this->id,

    $
    予約,

    );

    }

    30

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    {

    public function run(
    接種券番号 $
    接種券番号,
    自治体番号 $
    自治体番号,
    予約接種日 $
    予約接種日): void

    {

    //
    データベースからドメインオブジェクトを取得

    $
    接種者 = $this->query->find($
    接種券番号, $
    自治体番号);

    if ($
    接種者 === null) {

    throw new PreconditionException('
    該当する接種者が存在しません');

    }

    //
    ドメインロジックを実行

    $
    接種者 = $
    接種者->
    予約登録(new
    予約($date));

    //
    結果ドメインオブジェクトをデータベースに保存

    $this->command->store($
    接種者);

    }

    }

    40

    View Slide

  41. モデル、コードの改善
    41

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    {

    case
    未予約;

    case
    予約完了;

    case
    接種完了;

    }

    45

    View Slide

  46. 接種者クラスの変更
    コンストラクタで接種ステータスを追加。初期値は未予約とする。
    final class
    接種者

    {

    public function __constructor(

    private
    接種者Id $id,

    private
    接種ステータス $
    接種ステータス =
    接種ステータス::
    未予約,

    private ?
    予約 $
    予約 = null,

    ) {}

    46

    View Slide

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

    {

    if ($this->
    接種ステータス !==
    接種ステータス::
    未予約) {

    throw new InvalidOperationException('
    現在のステータスで予約できません');

    }
    return new self(

    $this->id,

    接種ステータス::
    予約完了,

    $
    予約,

    );

    }

    47

    View Slide

  48. 予約ユースケースは変更なし
    ドメインクラスの変更のみなので、ユースケースクラスは変更無し。
    final class
    予約登録UseCase

    {

    public function run(

    接種券番号 $
    接種券番号,

    自治体番号 $
    自治体番号,

    予約接種日 $date,

    ): void {

    $
    接種者 = $this->query->findBy
    接種券番号And
    自治体番号($
    接種券番号, $
    自治体番号);

    if ($
    接種者 === null) {

    throw new PreconditionException('
    該当する接種者が存在しません');

    }
    $
    接種者 = $
    接種者->
    予約登録(new
    予約($date));

    $this->command->store($
    接種者);

    }

    }

    48

    View Slide

  49. まとめ
    49

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide