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

明日から使えるアーキテクチャ 独立したコアレイヤパターン / independent-core-layer-pattern-phpconsen2019

shin1x1
January 26, 2019

明日から使えるアーキテクチャ 独立したコアレイヤパターン / independent-core-layer-pattern-phpconsen2019

PHP カンファレンス仙台 2019

shin1x1

January 26, 2019
Tweet

More Decks by shin1x1

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. View Slide

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

    View Slide

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

    View Slide

  9. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. Eloquent
    が PDO
    に変わっても良い

    View Slide

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

    View Slide

  15. 既存実装への適用

    View Slide

  16. https://github.com/shin1x1/phpconsen2019

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. routes/api.php
    (ルーティング)
    use App\Http\Actions\AddPoint\AddPointAction;
    $router->put('/customers/add_point',
    AddPointAction::class);

    View Slide

  21. AddPointAction
    namespace App\Http\Actions\AddPoint;
    (snip)
    class AddPointAction
    {
    private $customer;
    private $customerPoint;
    public function __construct(
    EloquentCustomer $customer
    , EloquentCustomerPoint $customerPoint
    ) {
    $this->customer = $customer;
    $this->customerPoint = $customerPoint;
    }

    View Slide

  22. 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);
    }

    View Slide

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

    View Slide

  24. 独立したコアレイヤパターンの適用
    [core]
    コアロジックの抽出
    [core]
    コアロジックで必要なインターフェイスを定

    [app]
    アダプタの実装
    [app]
    コアロジックの実行

    View Slide

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

    View Slide

  26. [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);
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. 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);
    }
    }

    View Slide

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

    View Slide

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

    View Slide

  34. [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]);
    }

    View Slide

  35. モチベーション

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  40. さらなるアイデア
    コアレイヤの依存を CI
    でチェック
    コアレイヤ以外に依存していればエラー
    コアレイヤを Composer
    パッケージ化
    アプリケーションは別パッケージとして読む
    コアレイヤの DSL

    より抽象化して、PHP
    以外の言語でも動作

    View Slide

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

    View Slide

  42. Q?
    @shin1x1

    View Slide