Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

開発現場での Tips を 解説した本 Laravel 5.5(LTS) 対応 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

背景 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

A. 対象ドメインが違うから フレームワーク = Web アプリケーション開発 アプリケーション = EC サイト、チャットサービ ス、業務システム等々 ドメインは重なるが一致するわけではない Web アプリケーションは効率的に作れるが、 EC サイトを効率化するのではない ドメイン特化のパッケージなどの方が近い 7

Slide 8

Slide 8 text

MVC + Service ユースケースをサービスとして切り出す Web アプリケーションの一部の関心事は分離でき た HTTP に関する事はサービスには入れない その他の技術詳細(RDBMS 等)が分離できず 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

14

Slide 15

Slide 15 text

15

Slide 16

Slide 16 text

16

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

実装例 25

Slide 26

Slide 26 text

実装 API 顧客サービスのポイント加算 API PUT /customers/add_point customer_id = 会員ID add_point = 加算ポイント 顧客ポイントが加算される 更新後のポイントを JSON で返す 26

Slide 27

Slide 27 text

実装の流れ ユースケースを実装 必要なインターフェイスを定義 この時点でテスト可能 インターフェイスを満たすアダプタを実装 ユースケースを実行する Controller/Action を実装 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Laravel アプリケーション app/ <-- for Laravel packages/ <-- Point application Acme/ Point/ Core/ <--- コアレイヤ Application/ <--- アプリケーションレイヤ 独自ディレクトリに配置 29

Slide 30

Slide 30 text

ユースケース 事前条件 加算ポイントが 1 以上の整数である 会員 ID がデータベースに存在する 顧客ポイントを加算 更新後の顧客ポイントを返す 30

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

[Core] AddPointUseCaseCommand interface AddPointUseCaseCommand { public function addCustomerPoint( int $customerId, int $addPoint): void; } 顧客ポイントの加算 35

Slide 36

Slide 36 text

[Core] AddPointUseCase インターフェイスを満たすインスタンスを コンストラクタで受け取る final class AddPointUseCase { private $query; private $command; public function __construct( AddPointUseCaseQuery $query, AddPointUseCaseCommand $command ) { $this->query = $query; $this->command = $command; } 36

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

[Core] ユースケーステスト この段階でユースケースのテストが書ける 技術詳細はとりあえずモックを与える 無名クラスでインターフェイスを実装 $this->mockCommand = new class implements AddPointUseCaseCommand { public function addCustomerPoint(int $customerId, int $addPoint): void { } }; 38

Slide 39

Slide 39 text

[ アプリケーション] アダプタ コアレイヤで定義したインターフェイスを実装 フレームワークの機能を普通に利用 Eloquent 、QueryBuilder 、PDO など 一つのクラスで複数インターフェイス実装も可 39

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

サービスコンテナに登録 DI コンテナ(サービスコンテナ)でバインド アダプタクラスをユースケースに与える $this->app->bind(AddPointUseCase::class, function () { $adapter = $this->app->make(AppAddPointAdapter::class); return new AddPointUseCase($adapter, $adapter); }); 41

Slide 42

Slide 42 text

ユースケース実行 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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

コアレイヤパターン + ドメインモデル 44

Slide 45

Slide 45 text

コアレイヤでドメインモデルを活用 アプリケーションドメインをモデルで表現 顧客 ID = CustomerId 加算ポイント = AddPoint 顧客ポイント = Point コアレイヤにドメインモデルを実装 レイヤ間のデータ受け渡しもモデルを利用 45

Slide 46

Slide 46 text

46

Slide 47

Slide 47 text

ユースケース 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

Slide 48

Slide 48 text

インターフェイス 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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

コアレイヤパターンの現場 50

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

ルールの明確化 守れるシンプルなルールがある コアレイヤ = フレームワークなし アプリケーションレイヤ = フレームワークあり レビュー時の視点が定まる 53

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Q? @shin1x1 55

Slide 56

Slide 56 text

56

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

フレームワークとの分離例 PHP 5.6 -> 7.3 / Laravel 4.2 -> 5.5 (LTS) MVC + Service 効果的だった施策 独自ディレクトリにアプリケーション配置 Service に HTTP は持ち込まない Service は Eloquent に依存 Eloquent はそれほど大きく変わってなかった 変更箇所が多いと大変だった 58

Slide 59

Slide 59 text

単純な CRUD でも分離する? フレームワークの機能のみで実装できるなら避け る方法も コアレイヤで実装すると、ユースケースが 1 行の み このユースケースはそれだけシンプルなことは 分かる 59