Shin x Blog

PHPをメインにWebシステムを開発してます。Webシステム開発チームの技術サポートも行っています。

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

独立したコアレイヤは、アプリケーション実装パターンである。以下のような特徴を持つ。

  • アプリケーションを、何を実現するのか(What)と、どのように実現するのか(How)に分ける。
  • What は、コアレイヤに実装する。ユースケースやドメインロジックを実装する。フレームワークやライブラリには依存しない。UI やデータベースからは独立している。
  • How は、サービスレイヤ(仮)に実装する。フレームワークやライブラリを活用して、ユースケースが要求する技術詳細を実装する。
  • コアレイヤが必要な技術詳細はポート(インタフェース)で定義し、これに依存する。サービスレイヤでは、それに対するアダプタを実装する。

ここでは、Web アプリケーションを念頭においているが、内容は Web アプリケーションに特化するものではなく、他種のアプリケーションでも適用可能と考える。

モチベーション

  • 技術詳細からの独立

コアレイヤでは、ユースケースやドメインを記述するが、UI、フレームワークやサードパーティライブラリ、外部ミドルウェア通信等の詳細技術とは独立しているので、処理の流れやドメインロジックといった何を実現するのかを平易に記述していくだけで良い。こうしたコードは、後に他の開発者がコードを見た時にもアプリケーションが実現したいことが把握しやすい。

  • フレームワーク、サードパーティライブラリからの独立

コアレイヤは、フレームワークやサードパーティライブラリには依存しない。こうした機能は、ポート経由で間接的に利用する。あくまでコアレイヤは、ポートに依存しているだけでその詳細には関与しない。こうしておくことで、フレームワークやライブラリのバージョンアップや入れ替えが発生しても、その影響はサービスレイヤで実装するアダプタに留まり、コアレイヤには及ばない。

  • サービスレイヤからの独立

コアレイヤは、サービスレイヤにも依存しないので、単体のアプリケーションとして利用することも可能である。つまり、テストランナーを使えば、単体でテストを実行できる。また、異なるサービスレイヤ、例えば Web アプリケーションとバッチアプリケーションがあっても、同じコアレイヤを使うことができる。

  • コアレイヤからの独立

サービスレイヤについても、コアレイヤから独立していることで、要求される技術に特化した処理を実装すれば良くなる。HTTP リクエストのハンドリング、レスポンス生成、データベースアクセス、メール送信といったものだ。こうしたものは、汎用フレームワークやライブラリで提供されているので、それらをアダプタとして、コアレイヤに提供すれば良い。

  • シンプルなルール

本パターンで定めているのは、レイヤをコアレイヤとサービスレイヤの 2 つに分ける、コアレイヤは外部の技術詳細に依存せずに自身で定義したポートに依存する、だけである。

このシンプルなルールにより、小さなアプリケーションにも適用しやすいものとなっている。

全体

下記に独立したコアレイヤパターンのイメージ図を示す。

f:id:shin1x1:20180510235928p:plain

この図を見れば、ヘキサゴナルアーキテクチャやオニオンアーキテクチャ、クリーンアーキテクチャを想起するだろう。本パターンは、ヘキサゴナルアーキテクチャ( Ports and Adapters パターン)の一種である。コアレイヤを中心におき、外部にある実装はインタフェース経由で利用する。

前述したアーキテクチャと異なるのは、レイヤ構造を 2 つにしている点とポートの種類や役割を限定していないことである。例えば、ヘキサゴナルアーキテクチャでは、ポートをプライマリとセカンダリに区分している。細かな違いではあるが、定義を明確にするために新たな名称を付けた。

サンプルアプリケーション

本エントリで提示するコードは、下記リポジトリにある。Docker 開発環境を構築できるので、テストなど動かしてみると良いだろう。

github.com

コアレイヤ

コアレイヤは、ユースケースとユースケースが利用するサービスのポート(インタフェース)を実装する。ユースケースは、想定される処理を順に記述していく。データベースアクセスなど技術詳細を利用したい場合、必要な API をインタフェースとして実装して、それに依存しておく。ユースケース内では、このインタフェースを利用して値の取得や保存などの処理を行う。

アプリケーションドメインに関する実装、エンティティや ValueObject、ドメインサービスなど、もコアレイヤに含める。ただ、これは必須ではなく、シンプルなユースであればこうしたドメインモデルを使わずにユースケースを実装する場合もある。

このレイヤに技術詳細は含まないので、Web アプリケーションフレームワークやサードパーティライブラリなどには依存しない。ただし、データやアルゴリズムなどを提供するライブラリについては依存する場合がある。例えば、PHP であれば、日付時間ライブラリである Carbon や Chronos 、データ構造ライブラリの php-ds*1 などの依存は考えられる。

以下に一つの例を示す。ここでは、銀行口座の情報を取得するユースケースを実装している。

なお、本エントリ全体で例示するソースコードは、シンプルな銀行口座アプリケーションのものである。また、AccountNumber や Account などはドメインモデルとして、このレイヤに含まれているとする。

<?php
declare(strict_types=1);

namespace Acme\Account\UseCase\GetAccount;

use Acme\Account\Domain\Exceptions\NotFoundException;
use Acme\Account\Domain\Models\Account;
use Acme\Account\Domain\Models\AccountNumber;

final class GetAccount
{
    private $query;

    public function __construct(GetAccountQueryPort $query)
    {
        $this->query = $query;
    }

    public function execute(AccountNumber $accountNumber): Account
    {
        return $this->query->findAccount($accountNumber);
    }
}

interface GetAccountQueryPort
{
    public function findAccount(AccountNumber $accountNumber): Account;
}

GetAccount クラスがユースケース、GetAccountQueryPort インタフェースがこのユースケースが利用するポートである。ユースケースでは、コンストラクタにポートを引数で受け取る。execute メソッドにユースケースを実装する。このメソッドでは、単に GetAccountQueryPort インタフェースの findAccount メソッドを実行してその戻り値を返すだけである。

execute メソッドを見れば、このユースケースが何をするかは一目瞭然である。たが、実際に口座情報がどこにあり、それをどのように行うかはコアレイヤは知らない。

ポートには、ユースケースごとに定義する。読み取り処理を担う Query ポート、データ保存やメール送信など副作用を伴う処理を担う Command ポート、アプリケーション全体で横断的に利用する Transaction ポートなど考えられる。ただ、これは本パターンで規定しておらず、あくまで一例である。

ユースケースはフレームワークやサードパーティライブラリには依存せず、データベースなどにも直接接続しないので、テスト用データベースのセットアップなどなくても簡単にテストが書ける。以下が GetAccount ユースケースのテストである。

<?php
declare(strict_types=1);

namespace Acme\Test\Account\UseCase;

use Acme\Account\Domain\Exceptions\NotFoundException;
use Acme\Account\Domain\Models\Account;
use Acme\Account\Domain\Models\AccountNumber;
use Acme\Account\UseCase\GetAccount\GetAccount;
use Acme\Account\UseCase\GetAccount\GetAccountQueryPort;
use PHPUnit\Framework\TestCase;

final class GetAccountTest extends TestCase
{
    /**
     * @test
     * @throws NotFoundException
     */
    public function execute()
    {
        $sut = new GetAccount(
            new class implements GetAccountQueryPort
            {
                public function findAccount(AccountNumber $accountNumber): Account
                {
                    return Account::ofByArray([
                        'account_number' => $accountNumber,
                        'email' => 'a@example.com',
                        'balance' => 1000,
                    ]);
                }
            }
        );

        $accountNumber = 'A0001';
        $actual = $sut->execute(AccountNumber::of($accountNumber));

        $this->assertSame($accountNumber, $actual->accountNumber()->asString());
        $this->assertSame(1000, $actual->balance()->asInt());
    }

    /**
     * @test
     * @expectedException \Acme\Account\Domain\Exceptions\NotFoundException
     */
    public function error_account_not_found()
    {
        $sut = new GetAccount(
            new class implements GetAccountQueryPort
            {
                public function findAccount(AccountNumber $accountNumber): Account
                {
                    throw new NotFoundException();
                }
            }
        );

        $sut->execute(AccountNumber::of('Z9999'));
    }
}

GetAccountQueryPort をインジェクトするだけでユースケースは実行できるので、無名クラスでこのインタフェースを実装して与えている。GetAccountTest クラスの execute メソッドでは正常ケースとして、Account インスタンスを返している。一方、error_account_not_found メソッドでは、口座情報が見つからないケースとして例外を送出している。このようにユースケースではインタフェースに依存しているだけなので、インジェクトするクラスの実装をいかようにも変えることができる。

このユースケースは、あまりにシンプルでイメージが沸かないということであれば、後述する口座間送金ユースケースの例を見ると良い。

サービスレイヤ

サービスレイヤは、UI や データベースなどアプリケーション外部との連携を実装する。

このレイヤでは、2つの責務を担う。1 つ目は、UI からのアクションを契機にユースケースを実行することである。コアレイヤから提供されるユースケースクラスを実行することになる。2 つ目は、実行するユースケースが依存しているポートに対するアダプタを実装することだ。インタフェースを満たせば、その実装の詳細は自由である。

先の GetAccount ユースケースに対する本レイヤの実装を見ていこう。ここでは、Laravel 5.6 を利用しているものとする。

まず、GetAccount ユースケースが要求している GetAccountQueryPort に対するアダプタ実装が以下となる。

<?php
declare(strict_types=1);

namespace App\Action\GetAccount;

use Acme\Account\Domain\Exceptions\NotFoundException;
use Acme\Account\Domain\Models\Account;
use Acme\Account\Domain\Models\AccountNumber;
use Acme\Account\UseCase\GetAccount\GetAccountQueryPort;
use App\Eloquents\EloquentAccount;

final class GetAccountAdapter implements GetAccountQueryPort
{
    private $account;

    public function __construct(EloquentAccount $account)
    {
        $this->account = $account;
    }

    public function findAccount(AccountNumber $accountNumber): Account
    {
        $account = $this->account->findByAccountNumber($accountNumber);
        if (is_null($account)) {
            throw new NotFoundException(sprintf('account_number %s not found', $accountNumber->__toString()));
        }

        return $account->toModel();
    }
}

GetAccountAdapter クラスでは、GetAccountQueryPort を実装している。内部では、Eloquent を利用して、データベースから口座情報を取得し、Account インスタンスとして返している。ここでは、コンストラクタで EloquentAccount をインジェクトしているが、ファサードを使っても問題無い。サービスレイヤでは、フレームワークの機能を利用して、より開発しやすい方法で実装すれば良い。

Laravel にはサービスコンテナという DI コンテナがあるので、GetAccount クラス生成時にこのアダプタを与えるように定義しておく。ここでは、CoreServiceProvider を追加して、その中で DI コンテナに定義を追加している。

<?php
declare(strict_types=1);

namespace App\Providers;

use Acme\Account\UseCase\GetAccount\GetAccount;
use App\Action\GetAccount\GetAccountAdapter;

final class CoreServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        //
    }

    public function register(): void
    {
        $this->app->bind(GetAccount::class, function () {
            $adapter = app(GetAccountAdapter::class);

            return new GetAccount($adapter);
        });
    }
}

Laravel のルーティングからユースケースを実行する GetAccountAction は以下になる。

<?php
declare(strict_types=1);

namespace App\Action\GetAccount;

use Acme\Account\Domain\Models\AccountNumber;
use Acme\Account\UseCase\GetAccount\GetAccount;
use Illuminate\Http\Request;

final class GetAccountAction
{
    private $useCase;

    public function __construct(GetAccount $useCase)
    {
        $this->useCase = $useCase;
    }

    public function __invoke(Request $request, string $accountNumber)
    {
        $account = $this->useCase->execute(AccountNumber::of($accountNumber));

        return response()->json([
            'account_number' => $account->accountNumber()->asString(),
            'balance' => $account->balance()->asInt(),
        ]);
    }
}

コンストラクタで、先程 CoreServiceProvider でサービスコンテナに登録した GetAccount インスタンスが与えられる。ルーティングからは、__invoke メソッドが実行される。この中でパラメータのバリデーションなどを行って、ユースーケースを実行する。HTTP に関することは、このクラスで処理して、ユースーケースには渡さないようにする。ユースーケースからは、Account インスタンスが返されるので、これを JSON で出力している。

このように、アプリケーション外部やフレームワークなどに関する処理は全てこのレイヤで行う。

口座間送金ユースケース

より実際のアプリケーションに近いかたちとして、口座間の送金処理ユースケースの実装例を見る。

処理の流れ

  • 送金元口座番号、送金先口座番号、送金金額を指定する。
  • 送金元口座番号から送金元口座情報を取得する。
  • 送金先口座番号から送金先口座情報を取得する。
  • 送金元口座から送金金額を出金する。
  • 送金先口座に送金金額を入金する。
  • 送金元口座の取引ログに出金を記録する。
  • 送金先口座の取引ログに入金を記録する。
  • 送金元口座残高を保存する。
  • 送金先口座残高を保存する。

コアレイヤ

ユースケース TransferMoney クラスは以下のようになる。

まず、3 つのポートに依存している。TransferMoneyQueryPort はデータ取得のような読み取り処理を担うポート、TransferMoneyCommandPort はデータ保存やメール送信のような副作用を伴う処理を担うポートTransactionPort は、データベーストランザクションを担う汎用ポートである。

execte メソッドでは、送信元口座番号、送信先口座番号、送金金額、そして取引ログを記録するために処理日時を引数に取る。これは、サービスレイヤから与えられる。このメソッドでは、Query ポートで口座情報取得、ドメインルール検証、ドメインロジック(送金)、Command ポートで結果の保存、通知といった処理を行っている。一連の処理は、Transaction ポートでトランザクション内で実行するようにしている。

<?php
// (snip)
final class TransferMoney
{
    private $query;
    private $command;
    private $transaction;

    public function __construct(
        TransferMoneyQueryPort $query,
        TransferMoneyCommandPort $command,
        TransactionPort $transaction
    ) {
        $this->query = $query;
        $this->command = $command;
        $this->transaction = $transaction;
    }

    public function execute(
        AccountNumber $sourceNumber,
        AccountNumber $destinationNumber,
        Money $amount
    ): Balance {
        return $this->transaction->transaction(function () use ($sourceNumber, $destinationNumber, $amount) {
            [$source, $destination] = $this->query($sourceNumber, $destinationNumber);

            if ($source->accountNumber()->equals($destination->accountNumber())) {
                throw new DomainRuleException('source can not transfer to same account');
            }

            if ($source->balance()->lessThan($amount)) {
                $message = sprintf('source account does not have enough balance for transfer %s', $amount->asInt());
                throw new DomainRuleException($message);
            }

            $source->withdraw($amount);
            $destination->deposit($amount);

            $this->store($source, $destination, $amount, $now);

            $this->command->notify($source);

            return $this->query->findAccount($sourceNumber)->balance();
        });
    }
}

query メソッドでは、Query ポートを使って、口座番号から送金元、送信先口座情報を取得している。このユースケースでは後ほど、残高を更新するためにレコードロックを獲得している。この時、デッドロックを防ぐために口座番号の若い方からロックをかけるようにしている。

<?php

    private function query(AccountNumber $sourceNumber, AccountNumber $destinationNumber): array
    {
        if ($sourceNumber->lessThan($destinationNumber)) {
            $source = $this->query->findAndLockAccount($sourceNumber);
            $destination = $this->query->findAndLockAccount($destinationNumber);
        } else {
            $destination = $this->query->findAndLockAccount($destinationNumber);
            $source = $this->query->findAndLockAccount($sourceNumber);
        }
        return [$source, $destination];
    }

store メソッドでは、入金後の口座残高を保存している。また、送金処理に関する取引ログも合わせて記録している。

<?php

    private function store(Account $source, Account $destination, Money $amount, TransactionTime $now): void
    {
        $this->command->storeBalance($source->accountNumber(), $source->balance());
        $this->command->storeBalance($destination->accountNumber(), $destination->balance());
        $this->command->addTransaction(new Transaction(
            $source->accountNumber(),
            TransactionType::WITHDRAW(),
            $now,
            $amount,
            'transferred to ' . $destination->accountNumber()->asString()
        ));
        $this->command->addTransaction(new Transaction(
            $destination->accountNumber(),
            TransactionType::DEPOSIT(),
            $now,
            $amount,
            'transferred from ' . $source->accountNumber()->asString()
        ));
    }

この実装を見て分かるようにコアレイヤでは、トランザクションやレコードロックの順序などいわゆるドメインレイヤでは扱わないコードについても対象となり得る。どこまでをコアレイヤにすべきかということは本パターンでは特に定めない。

サービスレイヤ

サービスレイヤでは、ユースケースの実行と定義されたポートに対するアダプタを実装していく。

GetAccount と同様にルーティングに設定されたアクションでは下記のようにユースケースを実行している。

<?php

    public function __invoke(TransferMoneyRequest $request, string $accountNumber)
    {
        $validated = $request->validated();
        $balance = $this->useCase->execute(
            AccountNumber::of($accountNumber),
            AccountNumber::of($validated['destination_number']),
            Money::of((int)$validated['money']),
            TransactionTime::now()
        );

        return response()->json([
            'balance' => $balance->asInt(),
        ]);
    }

ポートに対するアダプタは下記のようになる。 https://github.com/shin1x1/independent-core-layer-laravel/blob/master/app/Action/TransferMoney/TransferMoneyAdapter.php

ここでは、Query ポートと Command ポートを同じアダプタクラスで実装している。ユースケースに対するポート実装が小さいものなら、こうした実装方法も考えられる。ポートにマッチするアダプタが提供されれば良いので、どのようにアダプタを実装するかは、本パターンでは定めない。

<?php

final class TransferMoneyAdapter implements
    TransferMoneyQueryPort,
    TransferMoneyCommandPort

コアレイヤ対象範囲

本パターンを実装する上で悩むところは、コアレイヤにどこまでを含めるかという点であろう。コアレイヤに技術詳細を含めないことは当然なのだが、どこまで抽象化するかは実装者に委ねられる。それを考えるヒントとして、TransferMoney の異なる実装を 2 つ提示する。

DDD スタイル

DDD パターンを活用して、よりドメインレイヤに近いかたちで実装したのが以下の例だ。ここでは、TransferMoneyAggregate という集約を利用して、Query や Command は、この集約単位で行っている。また、ドメインルール検証も TransferMoneySpec で実装している。

execute メソッドを見るとかなりシンプルな実装となっていること分かる。ただ、上記であったレコードロックの順番に関する実装は、サービスレイヤに任せるようになっているので、コアレイヤをやや小さくしたパターンともいえる。

<?php

(snip)

final class DDDStyleTransferMoney
{
    private $query;
    private $command;
    private $transaction;

    public function __construct(
        DDDStyleTransferMoneyQuery $query,
        DDDStyleTransferMoneyCommandPort $command,
        TransactionPort $transaction
    ) {
        $this->query = $query;
        $this->command = $command;
        $this->transaction = $transaction;
    }

    public function execute(
        AccountNumber $sourceNumber,
        AccountNumber $destinationNumber,
        Money $amount,
        TransactionTime $now
    ): Balance {
        return $this->transaction->transaction(function () use ($sourceNumber, $destinationNumber, $amount, $now) {
            $aggregate = $this->query->find($sourceNumber, $destinationNumber);

            if (!(new TransferMoneySpec())->isSatisfiedBy($aggregate, $amount)) {
                throw new DomainRuleException('TransferMoneySpec is not satisfied.');
            }

            $aggregate->transfer($amount, $now);

            $this->command->store($aggregate);
            $this->command->notify($aggregate->source());

            return $this->query->findAccount($sourceNumber)->balance();
        });
    }
}

interface DDDStyleTransferMoneyQuery
{
    public function find(AccountNumber $sourceNumber, AccountNumber $destinationNumber): TransferMoneyAggregate;

    public function findAccount(AccountNumber $accountNumber): Account;
}

interface DDDStyleTransferMoneyCommandPort
{
    public function store(TransferMoneyAggregate $aggregate): void;

    public function notify(Account $account): void;
}

手続き型スタイル

次は、手続き型によるユースケースの実装を見る。ここでは、ドメインモデルは一切利用せずに string や int、array といったプリミティブな型のみで実装している。ポートでの API 定義もプリミティブな型のみ利用しているので、アダプタも array を返すような実装となる。本パターンでは、このような手続き型スタイルで実装することも可能である。

<?php

(snip)

final class ProcedureStyleTransferMoney
{
    const TRANSACTION_TYPE_WITHDRAW = 'WITHDRAW';
    const TRNSACTION_TYPE_DEPOSIT = 'DEPOSIT';

    private $query;
    private $command;
    private $transaction;

    public function __construct(
        ProcedureStyleTransferMoneyQueryPort $query,
        ProcedureStyleTransferMoneyCommandPort $command,
        TransactionPort $transaction
    ) {
        $this->query = $query;
        $this->command = $command;
        $this->transaction = $transaction;
    }

    public function execute(
        string $sourceNumber,
        string $destinationNumber,
        int $amount,
        Chronos $now
    ): int {
        return $this->transaction->transaction(function () use ($sourceNumber, $destinationNumber, $amount, $now) {
            [$source, $destination] = $this->query($sourceNumber, $destinationNumber);

            if ($sourceNumber === $destinationNumber) {
                throw new DomainRuleException('source can not transfer to same account');
            }

            if ($source['balance'] < $amount) {
                $message = sprintf('source account does not have enough balance for transfer %s', $amount);
                throw new DomainRuleException($message);
            }

            $source['balance'] -= $amount;
            $destination['balance'] += $amount;

            $this->store($source, $destination, $amount, $now);

            $this->command->notify($source);

            return $this->query->findAccount($sourceNumber)['balance'];
        });
    }

    private function query(string $sourceNumber, string $destinationNumber): array
    {
        if ($sourceNumber < $destinationNumber) {
            $source = $this->query->findAndLockAccount($sourceNumber);
            $destination = $this->query->findAndLockAccount($destinationNumber);
        } else {
            $destination = $this->query->findAndLockAccount($destinationNumber);
            $source = $this->query->findAndLockAccount($sourceNumber);
        }

        return [$source, $destination];
    }

    private function store(array $source, array $destination, int $amount, Chronos $now): void
    {
        $this->command->storeBalance($source['account_number'], $source['balance']);
        $this->command->storeBalance($destination['account_number'], $destination['balance']);

        $this->command->addTransaction([
            'account_number' => $source['account_number'],
            'transaction_type' => self::TRANSACTION_TYPE_WITHDRAW,
            'transaction_time' => $now,
            'amount' => $amount,
            'comment' => 'transferred to ' . $destination['account_number'],
        ]);
        $this->command->addTransaction([
            'account_number' => $destination['account_number'],
            'transaction_type' => self::TRANSACTION_TYPE_DEPOSIT,
            'transaction_time' => $now,
            'amount' => $amount,
            'comment' => 'transferred from ' . $source['account_number'],
        ]);
    }
}

interface ProcedureStyleTransferMoneyQueryPort
{
    public function findAndLockAccount(string $accountNumber): array;

    public function findAccount(string $accountNumber): array;
}

interface ProcedureStyleTransferMoneyCommandPort
{
    public function storeBalance(string $accountNumber, int $balance): void;

    public function addTransaction(array $transaction): void;

    public function notify(array $account): void;
}

実装アイデア

レイヤでパッケージを分ける

コアレイヤは、それ単独で成り立つものなので、パッケージを分けてしまう方法も考えられる。ここでいうパッケージとは、ビルドや配布ができる単位で、PHP であれば composer パッケージ、Java なら jar ファイルといったものだ。このようにパッケージを分けることで、コアレイヤが依存する外部パッケージを必要最低限なものに制限でき、ビルドサイクルも分離できる。小さなアプリケーションでは考慮する必要は無いが、多くのユースケースを抱える場合やリリースサイクルを分ける場合は一考の余地があるだろう。

コアレイヤの範囲

前述したとおり、コアレイヤにどこまで含めるのかが本パターンの肝となる。最も範囲を広げるなら、技術詳細を使う一歩手前、分かりやすいところではフレームワークやライブラリの機能を利用する直前までとなる。反対にコアレイヤを狭めていき、ドメインレイヤのみなどにもできるが、そうなるとサービスレイヤにアプリケーションロジックが多数入り込むことになるので、分離という意味では効果が薄くなるかもしれない。

ポートの種類

本エントリでは、ポートをユースケース毎に定義し、その種類を Query、Command、Transaction としている。これには他のアプローチも考えられる。例えば、Command であれば、Account の保存と Transaction の保存、通知を別のポートとして定義することも考えられる。また、ユースケース毎ではなく、複数のユースケースで共通に利用できるように、利用用途でポートを定義する方法もあるだろう。

DDD スタイルへの一歩目

すでに見てきたように本パターンは、様々なスタイルでの開発に適用できる。開発初期は、フレームワークの流儀で実装しておく。次に、本パターンを活用してコアレイヤを独立させる。ここまで来れば、コアレイヤはフレームワークから独立しているので、技術詳細から独立したドメインレイヤを作るのは難しくない。このように、ドメインレイヤを作る一歩目として利用する方法も考えられる。

さいごに

Webアプリケーション開発において、オープンソースフレームワークやライブラリの活用は当然のこととなっている。ただ、特にフレームワークに関しては、アプリケーションの基盤として包括的に利用するので密結合になる傾向がある。初期開発においては、フレームワークに密結合で開発することは、動くものを速く作ることができるという悪くない選択だ。しかし、こうしたアプリケーションを運用していくと、フレームワークのバージョンアップの度にアプリケーション全体に手を入れることになり、開発の俊敏性が失われたり、変更コストが大きくなることがある。

あくまで、アプリケーションが主であり、フレームワークはそれを実現する一手段、ツールに過ぎない。フレームワークを利用しつつも、いかにアプリケーションコアと分離するか、影響を受けないようにするかという観点から、このパターンを考案した。

開発当初から想像するより、意外とアプリケーションの寿命は長い。初期開発時時に、どこまで先を見越してレイヤ分離を行うかはケースによるだろうが、本パターンであれば、それほど大きな工数をかけずに実現できるのではと見ている。長く運用され、価値を出し続けているアプリケーションに携わっている人のヒントになれば嬉しい。

参考