Slide 1

Slide 1 text

明日から使えるアーキテクチャ 独立したコアレイヤパターン 2019/01/26 PHP カンファレンス仙台 2019 @shin1x1

Slide 2

Slide 2 text

https://speakerdeck.com/shin1x1/phpcon2018- independent-core-layer-pattern

Slide 3

Slide 3 text

Agenda 独立したコアレイヤパターン 既存アプリケーションへの適用 モチベーション

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

独立したコアレイヤパターンとは https://blog.shin1x1.com/entry/independent- core-layer-pattern アーキテクチャパターン 2 つのシンプルなルール コアレイヤとアプリケーションレイヤに分ける コアレイヤはコアレイヤのみに依存する

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

コアレイヤ コアロジック(What )の実装 Plain PHP で実装 フレームワークに依存しない データ構造や操作などの一部ライブラリは利用

Slide 8

Slide 8 text

アプリケーションレイヤ 利用技術(How )の実装 データベース、外部 API 等の外部リソース操作 フレームワーク、ライブラリを活用 コアレイヤの実行(HTTP, CLI など)

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

課題 コアレイヤがアプリケーションレイヤに依存 Eloquent が変わるとコアレイヤも変更が必要

Slide 11

Slide 11 text

コアレイヤ インターフェイス コアレイヤで必要な外部リソースの操作を インターフェイスで定義 データベース、メール、外部 API など 実装はアプリケーションレイヤで行う DIP を利用 依存関係逆転の原則( SOLID 原則の D )

Slide 12

Slide 12 text

UseCase は UseCasePort に依存 Eloquent の変化の影響を受けない アプリケーションレイヤで UseCasePort を実装

Slide 13

Slide 13 text

Eloquent が PDO に変わっても良い

Slide 14

Slide 14 text

フレームワークが CakePHP になっても コアレイヤは影響を受けない

Slide 15

Slide 15 text

既存実装への適用

Slide 16

Slide 16 text

https://github.com/shin1x1/phpconsen2019

Slide 17

Slide 17 text

実装 API 顧客サービスのポイント加算 API 顧客情報 = customers テーブル 顧客ポイント = customer_points テーブル 対象顧客に顧客ポイントを加算 PUT /customers/add_point customer_id = 顧客 ID add_point = 加算ポイント 更新後のポイントを JSON で返す

Slide 18

Slide 18 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 }

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

routes/api.php (ルーティング) put('/customers/add_point', AddPointAction::class);

Slide 21

Slide 21 text

AddPointAction customer = $customer; $this->customerPoint = $customerPoint; }

Slide 22

Slide 22 text

public function __invoke(AddPointRequest $request): JsonResp { $customerId = filter_var( $request->json('customer_id'), FILTER_VALIDATE_INT); $addPoint = filter_var( $request->json('add_point'), FILTER_VALIDATE_INT); // 事前条件の検証 if ($addPoint <= 0) { throw new DomainRuleException('add_point should be e } if (!$this->customer->existsId($customerId)) { $message = sprintf('customer_id:%d does not exists' throw new DomainRuleException($message); }

Slide 23

Slide 23 text

// ポイント加算 $this->customerPoint->addPoint( $customerId, $addPoint ); // 加算ポイントの取得 $customerPoint = $this->customerPoint ->findPoint($customerId); return response()->json([ 'customer_point' => $customerPoint ]); } }

Slide 24

Slide 24 text

独立したコアレイヤパターンの適用 [core] コアロジックの抽出 [core] コアロジックで必要なインターフェイスを定 義 [app] アダプタの実装 [app] コアロジックの実行

Slide 25

Slide 25 text

[core] コアロジックの抽出 コアロジックをユースケースクラスに移動 簡易的には、Action から HTTP に関連する処理 以外を移動 HTTP レスポンスに必要な値を返す

Slide 26

Slide 26 text

[core] AddPointUseCase public function run(int $customerId, int $addPoint): int { if ($addPoint <= 0) { throw new DomainRuleException( 'add_point should be equals or greater than 1'); } if (!$this->customer->existsId($customerId)) { $message = sprintf( 'customer_id:%d does not exists', $customerId); throw new DomainRuleException($message); } $this->customerPoint->addPoint($customerId, $addPoint); return $this->customerPoint->findPoint($customerId); }

Slide 27

Slide 27 text

AddPointUseCasePort interface AddPointUseCasePort { public function existsId(int $customerId): bool; public function findPoint(int $customerId): int; public function addPoint( int $customerId, int $addPoint): void; }

Slide 28

Slide 28 text

[core] AddPointUseCase インターフェイス実装のインスタンスに与える run メソッドで $port を使うように変更 final class AddPointUseCase { /** @var AddPointUseCasePort */ private $port; public function __construct( AddPointUseCasePort $port) { $this->port = $port; }

Slide 29

Slide 29 text

[core] ユースケーステスト モックを与えればコアレイヤのみでテストできる /** * @test */ public function run_() { $useCase = new AddPointUseCase( $this->mockAdapter, ); $actual = $useCase->run(1, 100); $this->assertSame(200, $actual); }

Slide 30

Slide 30 text

[app] アダプタの実装 定義したインターフェイスをアダプタで実装 Eloquent を利用してデータベースアクセス 実装方法は何でも良い QueryBuilder or PDO or pg_*()

Slide 31

Slide 31 text

final class AddPointAdapter implements AddPointPort { // (snip) public function existsId(int $customerId): bool { return $this->customer->existsId($customerId); } public function findPoint(int $customerId): int { return $this->customerPoint ->findPoint($customerId); } public function addPoint( int $customerId, int $addPoint): void { $this->customerPoint->addPoint( $customerId, $addPoint); } }

Slide 32

Slide 32 text

[app] サービスコンテナに登録 ユースケースクラスのコンストラクタに アダプタを与えるようにサービスコンテナに登録 public function register(): void { $this->app->bind(AddPointUseCase::class, function () { $adapter = $this->app->make( AddPointAdapter::class); return new AddPointUseCase($adapter); }); }

Slide 33

Slide 33 text

[app] ユースケースを利用 アクションからユースケースを実行 HTTP の入出力とユースケース実行のみを担う

Slide 34

Slide 34 text

[app] AddPointAction 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, $addPoint); return response()->json( ['customer_point' => $customerPoint]); }

Slide 35

Slide 35 text

モチベーション

Slide 36

Slide 36 text

What と How の分離 How の更新の影響を What が受けにくくなる フレームワークバージョンアップ等 What が明確になる 要件がコードから読み取れる テストがしやすい レビューの視点が明確になる How は定められた役割に専念できる

Slide 37

Slide 37 text

既存パターン ドメインを中心にしたアーキテクチャ レイヤードアーキテクチャ クリーンアーキテクチャ 有用なパターンだが、too much に感じる時も アレンジして使っている現場も多い レイヤ構造とドメイン分析 それぞれ独立した話

Slide 38

Slide 38 text

既存パターンとの違い シンプルで適用しやすいルールに絞る これさえ守れば ok というもの レイヤ構造のみ モデリング云々は含まない DDD + コアレイヤパターン クリーンアーキテクチャの一種とも言える Simpli ed Clean Architecture

Slide 39

Slide 39 text

機械的に分離できる 既存アプリケーションを機械的に分離 コアロジックを抽出 外部リソース依存をインターフェイス化 インターフェイスをアダプタで実装 コアロジックの呼び出し レイヤ分けや複数のアダプタをどうまとめるか などを試行錯誤する場面はある

Slide 40

Slide 40 text

さらなるアイデア コアレイヤの依存を CI でチェック コアレイヤ以外に依存していればエラー コアレイヤを Composer パッケージ化 アプリケーションは別パッケージとして読む コアレイヤの DSL 化 より抽象化して、PHP 以外の言語でも動作

Slide 41

Slide 41 text

さいごに レイヤ分けを身近なものに スーパーヒーローがいなくても使える 簡単な API のリファクタリングから 自分たちのコードでやると分かりやすい ハンズオンとかもやりたい

Slide 42

Slide 42 text

Q? @shin1x1