独立したコアレイヤパターンによる PHP アプリケーションの実装 / phpcon2018-independent-core-layer-pattern

Ca17a082a30f4cbfed1d0a6dacbe3af2?s=47 shin1x1
December 15, 2018

独立したコアレイヤパターンによる PHP アプリケーションの実装 / phpcon2018-independent-core-layer-pattern

2018/12/15 PHP カンファレンス 2018

Ca17a082a30f4cbfed1d0a6dacbe3af2?s=128

shin1x1

December 15, 2018
Tweet

Transcript

  1. 独立したコアレイヤパターンによる PHP アプリケーションの実装 2018/12/15 phpcon 2018 @shin1x1

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

    https://php-genba.shin1x1.com/ 2
  3. 開発現場での Tips を 解説した本 Laravel 5.5(LTS) 対応 3

  4. Agenda 背景 独立したコアレイヤパターン 実装例 コアレイヤパターンの現場 4

  5. 背景 5

  6. アプリケーションとフレームワーク アプリケーションが主、フレームワークが従 アプリケーションのためのフレームワーク 密結合で開発するのが良い開発? フレームワークのアップデート問題 フレームワークとは適切な距離を取る なぜ? 6

  7. A. 対象ドメインが違うから フレームワーク = Web アプリケーション開発 アプリケーション = EC サイト、チャットサービ

    ス、業務システム等々 ドメインは重なるが一致するわけではない Web アプリケーションは効率的に作れるが、 EC サイトを効率化するのではない ドメイン特化のパッケージなどの方が近い 7
  8. MVC + Service ユースケースをサービスとして切り出す Web アプリケーションの一部の関心事は分離でき た HTTP に関する事はサービスには入れない その他の技術詳細(RDBMS

    等)が分離できず 8
  9. レイヤードアーキテクチャ DDD 界隈で登場するアーキテクチャ Eric Evans レイヤードアーキテクチャ ドメインを中心に 分離はできるが、それなりに複雑 実装パターンだけでも活かせるが、 ドメインレイヤの設計、分析が重要

    もう少しシンプルにできないか 9
  10. ベースのアイデア What と How をレイヤで分ける コアロジックと技術詳細(フレームワーク含む) の分離 守るべきシンプルなルールのみを決める 10

  11. 独立したコアレイヤパターン 11

  12. 独立したコアレイヤパターン アーキテクチャパターンの一つ 2 つのレイヤに分離 コアレイヤ コアロジックを実装 アプリケーションレイヤ(旧: サービスレイヤ) HTTP /

    RDBMS / 外部 API 等々の技術詳細 12
  13. 独立したコアレイヤパターン 2 つのレイヤに分離 コアレイヤ コアロジックを実装 アプリケーションレイヤ(旧: サービスレイヤ) HTTP / RDBMS

    / 外部 API 等々の技術詳細 13
  14. 14

  15. 15

  16. 16

  17. コアレイヤ コアロジック(What )を実装 Plain PHP フレームワークに依存しない データ構造や操作などの一部ライブラリは利用 コアレイヤのみに依存 技術詳細はインターフェイス経由で利用 ユースケース、ドメインモデルなど

    17
  18. 技術詳細インターフェイス コアレイヤが利用する技術詳細を インターフェイスで定義 DB アクセス Mail 送信 API 呼び出し インターフェイスを実装したインスタンスを

    コアレイヤに与える 18
  19. 要求をインターフェイスにする 実装の共通箇所をまとめるのではなく 必要な機能をインターフェイスにする手法 実装の数は問題ではない 依存関係逆転の法則(DIP ) 19

  20. 依存関係逆転の法則(DIP ) SOLID 原則の D Dependency inversion principle レイヤやモジュール間の依存関係を インターフェイスを利用して逆転させる

    20
  21. 依存関係逆転の法則(DIP ) ユースケースがアプリケーションレイヤに依存 21

  22. 依存関係逆転の法則(DIP ) ユースケースはインターフェイスに依存 アダプタがコアレイヤのインターフェイスに依存 依存関係が逆転している 22

  23. アプリケーションレイヤ 技術詳細(How )の実装 コアレイヤで定義したインターフェイスを実装 フレームワーク、ライブラリを活用 UI からコアレイヤの実行(HTTP, CLI など) 23

  24. 独立したコアレイヤパターン コアレイヤとアプリケーションレイヤに分離 コアレイヤ: コアロジック アプリケーションレイヤ: 技術詳細 コアレイヤからアプリケーションレイヤは インターフェイスを介して利用 24

  25. 実装例 25

  26. 実装 API 顧客サービスのポイント加算 API PUT /customers/add_point customer_id = 会員ID add_point

    = 加算ポイント 顧客ポイントが加算される 更新後のポイントを JSON で返す 26
  27. 実装の流れ ユースケースを実装 必要なインターフェイスを定義 この時点でテスト可能 インターフェイスを満たすアダプタを実装 ユースケースを実行する Controller/Action を実装 27

  28. https://github.com/shin1x1/phpcon2018- independent-core-layer 28

  29. Laravel アプリケーション app/ <-- for Laravel packages/ <-- Point application

    Acme/ Point/ Core/ <--- コアレイヤ Application/ <--- アプリケーションレイヤ 独自ディレクトリに配置 29
  30. ユースケース 事前条件 加算ポイントが 1 以上の整数である 会員 ID がデータベースに存在する 顧客ポイントを加算 更新後の顧客ポイントを返す

    30
  31. [Core] ユースケースクラス ユースケースをそのまま実装 ドメインルール検証、ビジネスロジック実行 アプリケーションレイヤへの要求 会員 ID の存在チェック 顧客ポイントの加算 顧客ポイントの取得

    31
  32. [Core] 要求インターフェイス ユースケースに必要なポート(インターフェイ ス)を定義 個別でも一つにまとめても良い 例 ユースケースごと 読み込み or 書き込み

    共通 32
  33. [Core] 要求インターフェイス例 ユースケース毎に定義 読み込み = Query データの状態を変えない 戻り値を返す 書き込み =

    Command データの状態を変える 戻り値は無い 33
  34. [Core] AddPointUseCaseQuery interface AddPointUseCaseQuery { public function existsCustomerId( int $customerId):

    bool; public function findPoint(int $customerId): int; } 顧客 ID の存在確認 顧客ポイント取得 34
  35. [Core] AddPointUseCaseCommand interface AddPointUseCaseCommand { public function addCustomerPoint( int $customerId,

    int $addPoint): void; } 顧客ポイントの加算 35
  36. [Core] AddPointUseCase インターフェイスを満たすインスタンスを コンストラクタで受け取る final class AddPointUseCase { private $query;

    private $command; public function __construct( AddPointUseCaseQuery $query, AddPointUseCaseCommand $command ) { $this->query = $query; $this->command = $command; } 36
  37. public function run(int $customerId, int $addPoint): int { // 事前条件の検証

    if ($addPoint <= 0) { throw new DomainRuleException(); } if (!$this->query->existsCustomerId($customerId)) { throw new DomainRuleException(); } // 顧客ポイントの加算 $this->command->addCustomerPoint( $customerId, $addPoint); // 加算後ポイントの取得 return $this->query->findPoint($customerId); } 37
  38. [Core] ユースケーステスト この段階でユースケースのテストが書ける 技術詳細はとりあえずモックを与える 無名クラスでインターフェイスを実装 $this->mockCommand = new class implements

    AddPointUseCaseCommand { public function addCustomerPoint(int $customerId, int $addPoint): void { } }; 38
  39. [ アプリケーション] アダプタ コアレイヤで定義したインターフェイスを実装 フレームワークの機能を普通に利用 Eloquent 、QueryBuilder 、PDO など 一つのクラスで複数インターフェイス実装も可

    39
  40. AppAddPointAdapter Query と Command を 1 クラスで実装 $this->customer = Eloquent

    final class AppAddPointAdapter implements AddPointUseCaseQuery, AddPointUseCaseCommand { // (snip) public function existsCustomerId( int $customerId): bool { return $this->customer->existsId($customerId); } // (snip) } 40
  41. サービスコンテナに登録 DI コンテナ(サービスコンテナ)でバインド アダプタクラスをユースケースに与える $this->app->bind(AddPointUseCase::class, function () { $adapter =

    $this->app->make(AppAddPointAdapter::class); return new AddPointUseCase($adapter, $adapter); }); 41
  42. ユースケース実行 Action クラスでユースケース実行 $customerId = filter_var( $request->json('customer_id'), FILTER_VALIDATE_INT); $addPoint =

    filter_var( $request->json('add_point'), FILTER_VALIDATE_INT); // ユースケースクラスを実行 $customerPoint = $this->useCase->run( $customerId, $addPoint ); return response()->json( ['customer_point' => $customerPoint]); 42
  43. curl で API 実行 $ curl "http://localhost:8000/api/customers/add_point" -X PUT -d

    '{"customer_id": 1, "add_point": 1}' -H "Content-Type: application/json" -H "Accept: application/json" | jq . { "customer_point": 101 } 43
  44. コアレイヤパターン + ドメインモデル 44

  45. コアレイヤでドメインモデルを活用 アプリケーションドメインをモデルで表現 顧客 ID = CustomerId 加算ポイント = AddPoint 顧客ポイント

    = Point コアレイヤにドメインモデルを実装 レイヤ間のデータ受け渡しもモデルを利用 45
  46. 46

  47. ユースケース public function run(CustomerId $customerId , AddPoint $addPoint): Point {

    if (!$this->query->existsCustomerId($customerId)) { throw new DomainRuleException(); } $this->command->addCustomerPoint( $customerId, $addPoint); return $this->query->findPoint($customerId); } 47
  48. インターフェイス interface AddPointUseCaseQuery { public function existsCustomerId( CustomerId $customerId): bool;

    public function findPoint( CustomerId $customerId): Point; } interface AddPointUseCaseCommand { public function addCustomerPoint( CustomerId $customerId, AddPoint $addPoint): void; } 48
  49. Action public function __invoke(AddPointRequest $request): JsonResponse { $customerId = filter_var(

    $request->json('customer_id'), FILTER_VALIDATE_INT); $addPoint = filter_var( $request->json('add_point'), FILTER_VALIDATE_INT); $customerPoint = $this->useCase->run( CustomerId::of($customerId), // ドメインモデル AddPoint::of($addPoint) // ドメインモデル ); return response()->json( ['customer_point' => $customerPoint]); } 49
  50. コアレイヤパターンの現場 50

  51. レイヤ分けに迷う はっきり分かれるところと曖昧なところがある 例:トランザクション トランザクション制御をインターフェイス化 実際の BEGIN / COMMIT / ROLLBACK

    は アプリケーションレイヤで実装 51
  52. コアレイヤへの入出力データ ユースケースへの引数、戻り値のデータ型は自由 スカラー型もしくはドメインモデルになる フレームワーク固有の型は避ける 例外: Laravel UploadedFile API テストには必要だったので妥協 PHP

    のアップロード機能自体が密結合... 52
  53. ルールの明確化 守れるシンプルなルールがある コアレイヤ = フレームワークなし アプリケーションレイヤ = フレームワークあり レビュー時の視点が定まる 53

  54. まとめ 独立したコアレイヤパターン コアレイヤとアプリケーションレイヤ コアレイヤからインターフェイスでアクセス 依存関係逆転の法則(DIP ) フレームワーク、ライブラリは素晴らしいもの! 54

  55. Q? @shin1x1 55

  56. 56

  57. 全員がこのパターンをやるべきか? これはあくまで一つの考え フレームワーク密結合でも、その状況で最適と考 えるなら ok 引き出しに入れておく 銀の弾丸は無い (上位レイヤの話は強くなりがち問題... 57

  58. フレームワークとの分離例 PHP 5.6 -> 7.3 / Laravel 4.2 -> 5.5

    (LTS) MVC + Service 効果的だった施策 独自ディレクトリにアプリケーション配置 Service に HTTP は持ち込まない Service は Eloquent に依存 Eloquent はそれほど大きく変わってなかった 変更箇所が多いと大変だった 58
  59. 単純な CRUD でも分離する? フレームワークの機能のみで実装できるなら避け る方法も コアレイヤで実装すると、ユースケースが 1 行の み このユースケースはそれだけシンプルなことは

    分かる 59