🌟

Laraveでのクリーンアーキテクチャー考察

2024/02/23に公開

そもそもなぜクリーンアーキテクチャーを考察するのか

DRY 原則や SOLID 原則という理論がエンジニア界隈では浸透している昨今ですが、実際の開発現場のソースコードを読み込んでみると必ずしもこれらの原則に則していない場合は多いのではないでしょうか。

そして、そういった開発環境でいざコーディングをしていくと、以下のような問題に直面するのではないでしょうか。

  • あるバグの修正をしたのだが、同じロジックが他の場所でも書かれていたようで重複箇所のバグは依然としてバグったままだった。

  • あるクラスを変更したが、依存性の方向性や範囲が把握しきれておらず、変更の影響で新たなバグを生んでしまった。

  • ビジネスロジックの変更を迫られたが、同じロジックが重複しすぎており修正範囲を特定するだけで一苦労。

  • 想定外の値の入力があり、バグが発生してしまった。

これらは、運用しながら仕様がコロコロ変わっていくようなスタートアップ系のプロダクトでは大なり小なりどこにもで発生しうる問題です。

これらの問題の共通の原因は、ソースコードに「可変性」が欠如している事です。

今回はこういった問題を解決できる糸口を掴むべく、クリーンアーキテクチャーを考察してみたいと思います。

なぜ「可変性」が失われていくのか

新しくプロダクトを新規で作成する時はまだソースコードのサイズは小さく、ちょっとした仕様の変更ならば比較的変更の「影響範囲」も肉眼で確認できるレベルなので、それほど神経質になることは無いと思います。

しかし、幸いにもプロダクトが軌道にのり、運用期間が長くなるにつれてソースコードのサイズは肥大化していき、ある処理がソースコード全体に与えている「影響範囲」も人間の肉眼だけでは把握できなくなっていき変更コストが増大します。

また、オブジェクト指向では Class 同士に「依存関係」というものが発生しますが、この「依存関係」に縛られて凝り固まってしまっている場合、仕様変更などで機能の取替を迫られた場合の変更が困難になります。

「依存関係」とは

依存というのは、例えばクラス A の機能がクラス B の機能を前提に作られている場合などを指し、この場合は「A は B に依存をしている」と言える。

ここで、すこし具体的な例を提示します。

/**
 * ユーザーの商品購入を実行するクラス
 */
class UserBuyProductService
{
	public function buy(int $userId, int $productId, int $amount): void
	{
		// 決済の処理を行う
		$stripePayment = new StripePayment();
		$stripePayment->payment($userId, $amount);

		// 決済が成功したらDatabaseなどに購入した事実を記録する。
	}
}

/**
 * 決済機能を司るクラス
 */
class StripePayment
{
	public function payment(int $userId, int $amount): void
	{
		// ここで決済処理を実行する。
	}
}

上記 UserBuyProductService は、あるユーザーが$productId を持つ商品を購入したときの処理を表しており、この購入処理の中で StripePayment クラスをインスタンス化して payment メソッドを使用することで購入処理を実現しています。

これは、UserBuyProductService クラスは StripePayment に依存していると表現することができ、この様な状態を密結合と呼びます。

「密結合」の何がいけないのか

結論から申し上げますと、密結合なソースコードは拡張性が低くなります。

例えば、前項の UserBuyProductService のケースでは決済機能として Stripe を使用していますが、これがビジネスサイドの要望で GMOPaymentGateway に変更を迫られたようなケースを考えてみましょう。

決済機能は「UserBuyProductService」だけで使われているとは限りません。

だから、既存のソースコードの中で「StripePayment」の影響を受けている処理を全て洗い出して漏れなく「GMOPaymentGateway」の処理にリファクタリングする必要があります。

大規模で複雑なプロダクトほど、「ただ切り替えるだけ」という変更要件に多大な変更コストが発生してしまうのです。

このようなケースの問題点を冷静にみつめると、ソースコード全体が「Stripe で決済をする」という「具体」に対して依存してしまっていることが問題だと仮定することができます。

では、具体の逆説である抽象にソースコードが依存した場合のケースを考えてみましょう。

「抽象に依存」とは

ここで、抽象とはなにか?と言うことを明確に定義する必要があります。

抽象とは「抽出」して「象(かたど)る」と書きます。

つまり、抽象とは、 「ある物事から共通項を抜き出して(抽出)、規格を作る(象る)」 と言い表すことが出来きるかと思います。

この規格を定義するのに、オブジェクト指向プログラミングでは「interface」を使います。

/**
 * 決済機能の実装クラスが実装していなければいけない
 * 機能の規格だけを定義する。
 * Interfaceは単なる規格(型)なのでインスタンス化されない。
 */
interface Payment
{
	/**
	 * 決済を実行する抽象メソッド
	 **/
	public function payment(int $userId, $amount): void;
}

上記のように、「interface」として、決済機能の振る舞いとして必要な振る舞いを「引き数」と「戻り値」の型と共に定義します。

そして、実際に商品購入処理を行っている「UserBuyProductServie」を、この interface に依存するようにします。

/**
 * ユーザーの商品購入を実行するクラス
 */
class UserBuyProductService
{
	private $paymentManager;

	public function __construct(Payment $peymentManeger)
	{
		$this->paymentManager = $peymentManeger;
	}

	public function buy(int $userId, int $productId, int $amount): void
	{
		// 決済の処理を行う
		// $stripePayment = new StripePayment();
		// $stripePayment->payment($userId, $amount);

		$this->paymentManager->payment($userId, $amount); // ここの処理がInerface経由になった

		// 決済が成功したらDatabaseなどに購入した事実を記録する。
	}
}

/**
 * 決済機能を司るクラス
 */
class StripePayment implements Payment
{
	public function payment(int $userId, int $amount): void
	{
		// ここでStripeでの決済処理を実行する。
	}
}

/**
 * 決済機能を司るクラス
 */
class GmoPaymentGateway implements Payment
{
	public function payment(int $userId, int $amount): void
	{
		// ここでGMOでの決済処理を実行する。
	}
}

上記の変更点は、「UserBuyProductService」のコンストラクタで「Payment」インターフェースを経由して受け取ったインスタンスを、メンバ変数であるpaymentMangerに代入し、paymentManger から payment メソッドを実行して決済を実現しています。

この様な方法を「コンストラクタインジェクション」と言いますが、ここでは「Payment 型」のインスタンスであればなんでも受け取れるようになります。

そのため、Payment インターフェースを実装した具象クラスであれば、如何なるインスタンスでも「UserBuyProductService」に外部注入することができます。

この様に、依存関係を Interfece などの抽象を通じて外部から注入することを「Dependency Injection」と言い、こうすることで「可変性」と「拡張性」を得ることができるのです。

また、Laravel の場合は「ServiceProvider」にて、「抽象クラス」と「具象クラス」の紐付けを設定することができます。

以下は、ServiceProvider の例

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Packages\Application\Product\ProductCreateInteractor;
use Packages\Application\Shop\ShopCreateInteractor;
use Packages\Application\User\UserCreateInteractor;
use Packages\Domain\CommonRepository\DataStoreTransactionInterface;
use Packages\Domain\CommonRepository\UuidGeneratorInterface;
use Packages\Domain\Models\Product\ProductRepository;
use Packages\Domain\Models\Shop\ShopRepository;
use Packages\Domain\Models\User\UserRepository;
use Packages\Infrastructure\EloquentRepository\DataStoreTransactionEloquentRepository;
use Packages\Infrastructure\EloquentRepository\ProductEloquentRepository;
use Packages\Infrastructure\EloquentRepository\ShopEloquentRepository;
use Packages\Infrastructure\EloquentRepository\UserEloquentRepository;
use Packages\Infrastructure\LaravelFeatureRepository\UuidGenerateLaravelFeatureRepository;
use Packages\UseCase\Product\Create\ProductCreateUseCaseInterface;
use Packages\UseCase\Shop\Create\ShopCreateUseCaseInterface;
use Packages\UseCase\User\Create\UserCreateUseCaseInterface;
use Packages\UseCase\User\Get\UserGetUseCaseInterface;
use Packages\Application\User\UserGetInteractor;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        /**
         * Eloquentリポジトリを登録
         */
        $this->app->bind(
            DataStoreTransactionInterface::class,
            DataStoreTransactionEloquentRepository::class
        );

        $this->app->bind(
            UserRepository::class,
            UserEloquentRepository::class
        );

        $this->app->bind(
            ShopRepository::class,
            ShopEloquentRepository::class
        );

        $this->app->bind(
            ProductRepository::class,
            ProductEloquentRepository::class
        );

        /**
         * ファサード系Repositoryを登録
         */
        $this->app->bind(
            UuidGeneratorInterface::class,
            UuidGenerateLaravelFeatureRepository::class
        );

        /**
         * UserCaseを登録
         */
        $this->app->bind(
            UserCreateUseCaseInterface::class,
            UserCreateInteractor::class
        );

        $this->app->bind(
            UserGetUseCaseInterface::class,
            UserGetInteractor::class
        );

        $this->app->bind(
            ShopCreateUseCaseInterface::class,
            ShopCreateInteractor::class
        );

        $this->app->bind(
            ProductCreateUseCaseInterface::class,
            ProductCreateInteractor::class
        );
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

これを活用することにより、例えば、「Payment インターフェースがコンストラクタインジェクションされる時には GmoPaymentGateway が自動的に具象クラスとしてインジェクションされる」といった設定を行うことができます。

この様に「抽象」に依存することによって、この先、決済機能を「Pay.jp に切り替えたいんだけど...」などという要求が発生た場合、「Payment」インターフェースを実装した「PayJp」クラスを作成し、ServiceProvider の「Payment」インターフェースに紐づく具象クラスを「PayJp」クラスに切り替えるだけで、ソースコードの全ての決済処理を「PayJp」に切り替える事ができます。

クリーンアーキテクチャーとは

前置きが長くなりましたが、ここでようやくクリーンアーキテクチャーについて見ていこうと思います。

クリーンアーキテクチャーといえば、この同心円のイメージが有名ですし、本家です。

簡潔に言うならば、コードをレイヤーに分け、依存性の方向を一方向にすることで保守性を高めようとするためのガイドラインだと言えるでしょう。

同心円の図からそれぞれの意味を理解する

基礎的な考え方を理解しなければ話は進まないので、まずは同心円の図について読解していきます。

依存の方向性

この矢印はレイヤー同士の依存関係の方向性を示している。

つまり、同心円の外側の要素が、内側の要素に向かって依存をするようにし、「Entities」などの内側の要素が外側に向かって依存しないようにすることを示しています。

同心円の図によれば、clean architecture の全ての要素の依存の方向性は全て「Entities」に向けられています。

では、この「Entites」とは一体何者なのでしょうか。

Entities

同心円の最も中心にある「Entities」がこれに当たります。

「ビジネスルール」を集める場所であり、そのアプリケーションの「名詞」にあたるモデルを定義する場所です。

「ビジネスルール」とは、そのアプリケーションがシステム化(自動化)されていないとした場合でも存在するドメインルールの事。

例えば、EC ショップのシステムであれば、User(出品者)、Shop(店舗)、Product(商品)などのドメイン知識が「Entity」クラスとして表現されることになります。

EC ショップというものは、システム化される以前も「出品者」と「店舗」と「商品」といった実体は存在し、ビジネスとはこれら名詞同士の関係性によって説明ができます。

「出品者」が「店舗」を開いて、「商品」を陳列した

このように、名詞そのものの属性や、名詞同士の繋がり合いのことを「ビジネスルール」と言います。

「Entities」の領域はこれらのビジネスルールを、他のレイヤーから保護するように隠蔽します。

そして、その他のレイヤーがこの隠蔽されたビジネスルールに依存することで、ビジネスルール(仕様)の変更を一箇所に集めることを実現するのです。

ここで、User のビジネスルールを考えてみましょう。User(出品者)は、名前などの情報を持っています。

簡単に例を書くと以下の様になる。

/**
 * Class UserEntity
 * 出品者の情報を表現するEntityクラス
 */
class UserEntity
{
	/** @var string **/
	private $id;

	/** @var string **/
	private $name;


	public function __construct(
		string $id = null,
		string $name
	) {
		$this->id = $id;
		$this->name = $name;
	}

	public function getId(): string
	{
		return $this->id;
	}

	public function getName(): string
	{
		return $this->name;
	}
}

ただしこれでは不完全で、上記のコードの様にクラスのメンバがプリミティブ型(string や int などの原始的な型)で定義されていると、なにかと不都合な事が生じます。

ここで登場するのが「ValueObject」という値を表現するオブジェクトです。

ValueObject

プリミティブ型は、「string は文字列、int は数値」とある程度、型の制約をしてくれるものの、ビジネスルールとしてはこの制約では不十分です。

たとえば上記の UserEntity クラスの場合、User の氏名である name の文字数は string 型の最小単位の空文字を許容して良いのでしょうか?

この様に、プリミティブ型の制約はビジネスルールの制約よりもザルであることを踏まえ、以下のように ValueObject を作成します。

/**
 * Class UserName
 * 出品者の氏名を表す値
 */
class UserName
{
	// 名前の最低文字数を表す定数
	public const MIN_LENGTH = 1;

	/** @var string **/
	private $_value;

	private function __construct(string $value)
	{
		$this->_value = $value;
	}

	public static function create(string $value): self
	{
		// 入力値の審査をする
		self::validation($value);

		return new self($value);
	}

	//値の審査をし、審査基準に満たなければエラーをスローする。
	private static function validation(string $value): bool
	{
		if (mb_strlen($value) < self::MIN_LENGTH) {
			throw new RuntimeException('名前は必須です。');
		}
	}

	// プリミティブな値を返すgetterメソッド
	public function value(): string
	{
		return $this->_value();
	}
}

このように ValueObject を作成し、先程の UserEntity をリファクタリングします。

/**
 * Class UserEntity
 * 出品者の情報を表現するEntityクラス
 */
class UserEntity
{
	/** @var string **/
	private $id;

	/** @var UserName **/
	private $name;


	public function __construct(
		string $id = null,
		UserName $name
	) {
		$this->id = $id;
		$this->name = $name;
	}

	public function getId(): string
	{
		return $this->id;
	}

	public function getName(): UserName
	{
		return $this->name;
	}
}

変更点は、コンストラクタでのnameの取得の型がstringからUserNameに変更され、UserEntityのメンバ変数であるname も UserName に型が変更されています。

これを実際にインスタンス化すると以下のようになります。

$userEntity = new UserEntity(
	'aaaa-bbbb-ccccc', // Userの識別子ID
	UserName::create('斎藤 一') // UserNameとして名前を生成
);

この時、もしも UserName::create の引き数に与えられたプリミティブの値が必要文字数を満たしていなければ、UserName の値審査機能がエラーをスローし、不正な値の混入を阻止します。

また、ビジネスルールの変更で、「UserName は最低文字数を 5 文字にしてほしい。」などという要求が出てきた場合も以下の箇所を一箇所変更するだけで全てのコードにルールの変更を反映させることができます。

class UserName
{
	// 名前の最低文字数を表す定数
	public const MIN_LENGTH = 5; // ここを変更するだけ。

	(以下省略......)

この様に、MIN_LENGTH の値を一箇所変えることで、ソースコード全体の UserName に関わる処理に対してルールの変更を伝播することができます。

ここまで、Entities や ValueObject をみてきましたが、これらを定義しただけではまだアプリケーションとして体をなしていません。

メモリー上で表現した Entity や ValueObject の状態を保存や再構築することが、アプリケーションをたり立たせる上で必要になります。

それを実現するのが「Gateways」という存在です。

Gateways

Laravel ではしばしば Repository パターンが採用されますが、この Repository が Gateways に該当します。

主にデータベースに対する保存や、再構築を担当するレイヤーです。

Laravel の場合はこの領域で Eloquent モデルの ORM を使うことで Database の操作を行います。

ここで問題となるのが、依存性の方向性です。

普通に Repository をクラスとして定義してしまうと、それを使う後述する UseCase 層から Gateways に依存の方向性が直接逆流してしまいます。

ここで Interface を使った「依存性逆転の原則」を使うことで、依存の方向性を維持することを実現します。

この例でみると、具象クラスである Reopsitory(Gateways 層)は interface を実装することで Interface へ依存のベクトルを延ばしています。(白抜きの矢印は実装による依存ベクトルを示す。)

この事を「依存性逆転の原則」と言います。

まず Interface から見ていきましょう。

interface UserRepositoryInterface
{
    /**
     * @param UserEntity $user
     * @return UserEntity
     */
    public function save(UserEntity $customer): UserEntity;
}

この様に、引数と戻り値の型を制約した Interface を定義します。

この抽象に依存する形で、具象クラスである Repository を作成します。

class UserRepository implements UserRepositoryInterface
{
    /**
	 * @param string $uuid PHPサイドで生成したUUIDの識別子
     * @param UserEntity $user
     * @return UserEntity
     */
    public function save(string $uuid, UserEntity $user): UserEntity
    {
		// 具象クラスとして処理を実装
		/**
		 * @var User ORマッパーのEloqunetUserモデル
		 */
		$ormUser = User::create([
			'id' => $uuid,
			'name' => $customer->getName()->value(),
		]);

		// UserEntityをORMから再構築して返却
		return new UserEntity(
			$ormUser->id,
			UserName::create($ormCustomer->name)
		);
    }
}

この様に、Interface に Repository が依存する形をとり、UseCase などからは Interface をサービスコンテナを通じて呼び出すようにすることで、依存性の方向性を維持することができる。

Geteways 層である Repository を Interface を通じて抽象に依存させることの意味は、UseCase からみた時の Repository の「可変性」を維持するためと言えます。

以下の図を御覧ください。

この様に、RepositoryInterface を実装した具象クラスが EloquentRepository と InmemoryRepository の 2 つ実装されているケースを考えます。

InmemoryRepository の用途は主にテスト用で、特に DataStore などに保存することなく、連想配列としてメンバ変数に値を保存するようなクラスです。

RepositoryInterface は現状ではこれら 2 つの具象クラスを bundle している形になっており、UseCase はこの RepositoryInterface へ依存しているため、どちらの具象クラスも外部注入(DI)することが叶います。

この性質を利用すれば、実際の運用の際は EloquentRepository を使用し、Test のときは InmemoryRepository を使用することができ、簡単にテストデータの構築を行うことができます。

また、技術の革新により新たに高性能な DataStore が生まれて、DataStore をそちらに乗り換えたい。

そして、その DataStore が Eloquent モデルに対応していない...。

その様な場合でも、RepositoryInterface を実装した新たな DataStore 用の具象 Repository を作成することで、UseCase などの層に影響を与えることなく入れ替えを行うことができるのです。

この様に、アプリケーションを特定の技術基盤に縛られないように「pluggable」(脱着可能)にして置くことで、ここでも「可変性」を守っているというわけです。

UseCase

UseCase はアプリケーションの API で、ドメインオブジェクトを操作し利用者の目的を達成することが役割で、1 つのビジネストランザクションとして定義します。

Entity は「名詞」に該当するということを上げましたが、UseCase は「活動(動詞)」を表現します。

「pluggable」な建て付けにするための interface とその実装がこれにあたります。

「pluggable」にすることで、テストの際はテスト用の UseCase の具象クラスとすり替えて、UI 層のテストを行いやす行くすることや、Database の建て付けなどがまだ確定していない段階でのフロントエンドの先行開発なども容易になります。

ここで具体的な UseCase の実装例を示します。

<?php

namespace Packages\Application\User;

use Packages\Domain\CommonRepository\UuidGeneratorInterface;
use Packages\Domain\Models\User\AuthUserEntity;
use Packages\Domain\Models\User\UserEmail;
use Packages\Domain\Models\User\UserId;
use Packages\Domain\Models\User\UserName;
use Packages\Domain\Models\User\UserPassword;
use Packages\Domain\Models\User\UserRepository;
use Packages\UseCase\User\Create\UserCreateRequest;
use Packages\UseCase\User\Create\UserCreateResponse;
use Packages\UseCase\User\Create\UserCreateUseCaseInterface;

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    /** @var UserRepository  */
    private $userRepository;

    /** @var UuidGeneratorInterface  */
    private $uuidGenerator;

    /**
     * UserCreateInteractor constructor.
     */
    public function __construct(
        UserRepository $userRepository,
        UuidGeneratorInterface $uuidGenerator
    ) {
        $this->userRepository = $userRepository;
        $this->uuidGenerator = $uuidGenerator;
    }

    public function __invoke(UserCreateRequest $request): UserCreateResponse
    {
        // Userのドメインモデルを生成
        // この時、全ての値の審査も行われる
        $user = new AuthUserEntity(
            UserId::create($this->uuidGenerator->generateUuidString()),
            UserName::create($request->getName()),
            UserEmail::create($request->getEmail()),
            UserPassword::create($request->getPassword())
        );

        // Repositoryに投げて永続化
        $user = $this->userRepository->create($user);

        // レスポンスとして返却する公開情報はResponseクラスで指定
        return new UserCreateResponse(
            $user->getId()->value(),
            $user->getName()->value(),
            $user->getEmail()->value()
        );
    }
}

UseCase は interface を通じて、後述する Controller から使用されますが、Controller と UseCase 間のデータのやり取りは DTO(Data Transfer Object)で行います。

例えば、上記のソースコードで UseCase の__invoke メソッドの引き数は UserCreateRequest という DTO クラスです。

この点において、Laravel を普段から使っている人であれば、「フロントエンドからの送信を受信している FormRequest をそのまま UseCase の引き数にしてしまえばよいのでは?」という疑問を抱くと思います。

ですが、ここでもう一度クリーンアーキテクチャーの同心円を思い出して頂きたい。

この図をみると、Frameworks のレイヤーは同心円上の最も外側に位置します。

Laravel の FormRequest クラスはこの Frameworks 層の住人であり、これに UseCase が依存することは依存方向性の逆流を意味します。

UseCase が特定の Framework などの技術基盤を前提に作られてしまうことは、「可変性」の低下を招くことになるため、DTO を使うことで「疎結合」を維持したいというモチベーションがここにはあります。

UserCreateResponse(OutputDTO)については、外部公開すべきデータを制御するために用いています。

Entity クラスのメンバ変数は private なので、これを公開範囲を指定し、公開可能なデータとして Controller に返却したいモチベーションがここにはあります。

Controllers

Controllers は入力をアプリケーション(UseCases)が要求する形に変更して伝えるのが役目です。

今回は Laravel を使うので、laravel の Controller がそれに当たります。

フロントエンドから送られてきたリクエストの内容を、UseCase が求める形に変換し、UseCase に伝えます。

さらに、http リクエストがレスポンスと対である兼ね合いで、laravel などのフレームワークの場合は UI への変換処理も Controller が担うことになります。

本来これらは Presenter の役割ですが、今回は割愛させて頂きます。

以上を踏まえ、俺的アーキテクチャーを考察

以上までを踏まえて、Laravel で実際にコードを書いてみます。

全体図

これは今回私なりに考えた、Laravel でクリーンアーキテクチャーに従う場合の全体図になります。

今回のサンプルアプリケーションも、基本的にこの画像のスキームに則って実装します。

薄いピンク色に囲まれたゾーンが、クリーンアーキテクチャーの同心円状の外側に位置する Frameworks 層にあたり、基本的な考え方としては Laravel の Eloquent モデルの機能や、FormRequest などの Validation 機能などはこのゾーンに閉じ込めてフル活用します。

そして、色の囲みがない UseCase や Entity などの領域は基本的に Laravel やその他の技術基盤に依存させないクリーンな状態を保つようにします。

サンプルアプリの仕様

User は Shop を複数持つことができて、Product を複数出品できる。

サンプルアプリリポジトリはこちら

実際にアプリケーションを構築する

上記の使用を全て上げてしまうと冗長になるので、ここでは、User の登録機能だけを取り出してサンプルアプリリポジトリの解説をしていきます。

まずは Entity と ValueObject を定義する

UserEntity

<?php


namespace Packages\Domain\Models\User;

/**
 * Class UserEntity
 * Userを表現するEntity
 * @package Packages\Domain\Models\User
 */
class UserEntity
{
    /** @var UserId */
    protected $id;

    /** @var UserName */
    protected $name;

    /** @var UserEmail */
    protected $email;

    /**
     * UserEntity constructor.
     * @param UserId $id
     * @param UserName $name
     * @param UserEmail $email
     * @param UserPassword $password
     */
    public function __construct(UserId $id, UserName $name, UserEmail $email)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }

    /**
     * @return UserId
     */
    public function getId(): UserId
    {
        return $this->id;
    }

    /**
     * @return UserName
     */
    public function getName(): UserName
    {
        return $this->name;
    }

    /**
     * @return UserEmail
     */
    public function getEmail(): UserEmail
    {
        return $this->email;
    }
}

UserId

<?php


namespace Packages\Domain\Models\User;

/**
 * Class UserId
 * Uesrの識別子であるIDを表すValueObject
 * @package Packages\Domain\Models\User
 */
class UserId
{
    /** @var string */
    private $_value;

    /**
     * UserId constructor.
     * @param string $userId
     */
    private function __construct(string $userId)
    {
        $this->_value = $userId;
    }

    public static function create(string $userId): self
    {
        return new self($userId);
    }

    public function value(): string
    {
        return $this->_value;
    }

    /**
     * UserId同士が等しいか審査する
     *
     * @param UserId $otherId
     * @return bool
     */
    public function isEquals(UserId $otherId): bool
    {
        return $this->_value === $otherId->value();
    }
}

UserName

<?php


namespace Packages\Domain\Models\User;

use RuntimeException;

/**
 * Class UserName
 * Userの氏名を表すValueObject
 * @package Packages\Domain\Models\User
 */
class UserName
{
    public const MIN_LNEGTH = 3;

    /** @var string */
    private $_value;

    /**
     * UserId constructor.
     * @param string $userName
     */
    private function __construct(string $userName)
    {
        self::validation($userName);

        $this->_value = $userName;
    }

    public static function create(string $userName): self
    {
        return new self($userName);
    }

    public function value(): string
    {
        return $this->_value;
    }

    private static function validation(string $value): void
    {
        if (mb_strlen($value) < self::MIN_LNEGTH) {
            throw new RuntimeException(sprintf('名前の最小文字数は%sです', self::MIN_LNEGTH));
        }
    }
}


ここで注目したいのは validation メソッドです。

この様に、ValueObject 自身に値審査機能があることで、そもそも不正な値で値オブジェクトを生成させないという建て付けになります。

また、今回のケースの「ユーザーの名前の文字数は 3 文字以上にしたい」といったルールを DomainObject である ValueObject に共通化することで、たとえばルールの変更を行う際に、DomainObject の変更をするだけで全体に変更を反映することができます。

class UserName
{
	// ここを変更するだけでプロジェクト全体のUserNameに関するルールが変更される。
    public const MIN_LNEGTH = 20;

UserEmail

<?php


namespace Packages\Domain\Models\User;

/**
 * Class UserEmail
 * UserのEmailアドレスを表すValueObject
 * @package Packages\Domain\Models\User
 */
class UserEmail
{
    /** @var string */
    private $_value;

    /**
     * UserId constructor.
     * @param string $userEmail
     */
    private function __construct(string $userEmail)
    {
        $this->_value = $userEmail;
    }

    public static function create(string $userEmail): self
    {
        return new self($userEmail);
    }

    public function value(): string
    {
        return $this->_value;
    }
}

AuthUserEntity

UserEntity は、登録処理のときは UserPassword が必須ですが、普段アプリケーション上の挙動を実現するためにはむしろ UserPassword は Entity の中には不要です。

そこで、今回は認証用に UserEntity を継承し拡張した AuthUserEntity を作成します。

<?php


namespace Packages\Domain\Models\User;

/**
 * Class AuthUserEntity
 * 認証関連用のUserEntity
 * @package Packages\Domain\Models\User
 */
class AuthUserEntity extends UserEntity
{
    /** @var UserPassword */
    private $password;

    public function __construct(UserId $id, UserName $name, UserEmail $email, UserPassword $password)
    {
        parent::__construct($id, $name, $email);
        $this->password = $password;
    }

    /**
     * @return UserPassword
     */
    public function getPassword(): UserPassword
    {
        return $this->password;
    }

}

追加点は$password というメンバ変数とその getter メソッドが追加されたに過ぎません。

UserPassword

<?php


namespace Packages\Domain\Models\User;

// TODO:ここでLaravelのファサードに依存してしまうのはあまり良くないので解決策を考える。
use Illuminate\Support\Facades\Hash;
use RuntimeException;

/**
 * Class UserPassword
 * Userのパスワードを示すValueObject
 * @package Packages\Domain\Models\User
 */
class UserPassword
{
    public const MIN_LENGTH = 8;

    /** @var string */
    private $_value;

    /**
     * UserId constructor.
     * @param string $userPassword
     */
    private function __construct(string $userPassword)
    {
        $this->_value = $userPassword;
    }

    public static function create(string $userPassword): self
    {
        self::validation($userPassword);

        return new self($userPassword);
    }

    public function value(): string
    {
        return $this->_value;
    }

    /**
     * 平文のパスワードをハッシュ化する処理
     *
     * @return string
     */
    public function getHashValue(): string
    {
        return Hash::make($this->_value);
    }

    private static function validation(string $userPassword): void
    {
        if (strlen($userPassword) < self::MIN_LENGTH) {
            throw new RuntimeException(sprintf('パスワードは最低%s文字です。', UserPassword::MIN_LENGTH));
        }
    }
}

ここで注目したいのは、getHashValue という関数です。

ここでは Database に保存する際に、平文のパスワードをハッシュ化するための機能を ValueObject 自身に実装しています。

ただし、コード上の TODO でも記載したとおり、このハッシュ化ロジックには Laravel のファサード機能を使っています。

DomainObject である ValueObject が「Laravel」という特定の技術基盤に依存している形になってしまっているので、このあたりのベストプラクティスはまだ私も考察中ですが、今回はこのままで進めたいと思います。

Entity が出揃ったところで Repository を作成

UserRepository

<?php

namespace Packages\Domain\Models\User;

use App\User;

interface UserRepository
{
    public function getById(UserId $userId): UserEntity;

    public function create(AuthUserEntity $userEntity): UserEntity;
}

Repository は上記を見てわかるように、ただのインターフェースです。

実際の具体的な処理はこのインターフェースを実装した、具象クラスにて実装します。

UserEloquentRepository

<?php

namespace Packages\Infrastructure\EloquentRepository;

use Packages\Domain\Models\User\UserEntity;
use Packages\Domain\Models\User\AuthUserEntity;
use Packages\Domain\Models\User\UserId;
use Packages\Domain\Models\User\UserRepository;
use Packages\Domain\Models\User\UserEntityFactory;
use App\User;

class UserEloquentRepository implements UserRepository
{
    public function create(AuthUserEntity $userEntity): UserEntity
    {
        $ormUser = User::create([
            'id' => $userEntity->getId()->value(),
            'name' => $userEntity->getName()->value(),
            'email' => $userEntity->getEmail()->value(),
            'password' => $userEntity->getPassword()->getHashValue(),
        ]);

        return UserEntityFactory::createFromORM($ormUser);
    }

    public function getById(UserId $userId): UserEntity
    {
        $ormUser = User::find($userId->value());

        return UserEntityFactory::createFromORM($ormUser);
    }
}

ここで注意したいのは、Laravel の Eloquent モデルを直接返さず、戻り値として UserEntity に載せ替えている点です。

これは、後述する UseCase 層に Laravel 特有の技術基盤である Eloquent モデルを漏れ出さないようにするためです。

Eloquent のことは EloquentRepository の中だけで完結しろというわけです。

また、ここで新しい概念である「UserEntityFactory」というものが登場しているのでこの点を補足説明致します。

UserEntityFactory

<?php


namespace Packages\Domain\Models\User;

use App\User;

/**
 * Class UserEntityFactory
 * UserのEloquentモデルからドメインオブジェクトである
 * UserEntityを生成する処理を担うクラス。
 *
 * @package Packages\Domain\Models\User
 */
class UserEntityFactory
{
    public static function createFromORM(User $user): UserEntity
    {
        return new UserEntity(
            UserId::create($user->id),
            UserName::create($user->name),
            UserEmail::create($user->email)
        );
    }
}

中身の処理は上記の様になっており、単純に EloquentModel の User を自前の DomainObject である UserEntity に載せ替えているだけの処理です。

この程度であれば、この Factory クラスの存在意義は感じにくいかもしれませんが、例えば多数のリレーション関係を持つモデルを Entity の載せ替える処理は、それ自体が複雑なロジックになります。

この「載せ替える」というロジックはそもそも Repository の責務そのものとは別問題なので、Factory というクラスに役割を分割するというわけです

DomainObject をまとめ上げて UseCase を作る

UserCreateUseCaseInterface

<?php


namespace Packages\UseCase\User\Create;

interface UserCreateUseCaseInterface
{
    public function __invoke(UserCreateRequest $request): UserCreateResponse;
}

UseCase もインターフェースで抽象化します。

理由は、バックエンドが完成するまえにでも UseCase をスタブと切り替えることでフロントエンドの先行開発などができるからです。

ここでもやはり「pluggable」な構成にすることで、柔軟性をもたせることができるというわけです。

UserCreateRequest

<?php


namespace Packages\UseCase\User\Create;


class UserCreateRequest
{
    /** @var string */
    private $name;

    /** @var string */
    private $email;

    /** @var string */
    private $password;

    /**
     * UserGetRequest constructor.
     * @param string $name
     * @param string $email
     * @param string $password
     */
    public function __construct(string $name, string $email, string $password)
    {
        $this->name = $name;
        $this->email = $email;
        $this->password = $password;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string
     */
    public function getEmail(): string
    {
        return $this->email;
    }

    /**
     * @return string
     */
    public function getPassword(): string
    {
        return $this->password;
    }
}

データの転送用の DTO になります。

Laravel の FormRequet を直接 UseCase 層に流入させてしまうと、UseCase 層が Laravel という特定の技術基盤に侵食されてしまいます。

この事を防ぐために DTO で連絡を取り合います。

UserCreateResponse

<?php


namespace Packages\UseCase\User\Create;


class UserCreateResponse
{
    /** @var string */
    private $id;

    /** @var string */
    private $name;

    /** @var string */
    private $email;

    /**
     * UserCreateResponse constructor.
     * @param string $id
     * @param string $name
     * @param string $email
     */
    public function __construct(string $id, string $name, string $email)
    {
        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }

    /**
     * @return string
     */
    public function getId(): string
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return string
     */
    public function getEmail(): string
    {
        return $this->email;
    }

	/**
     * 公開可能データを配列で返す処理
     *
     * @return array
     */
    public function toArray(): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
        ];
    }
}

こちらもデータ転送用の DTO です。

こちらは UseCase から Controller へのデータ転送用になりますが、これは Entity の private なメンバ変数を、公開範囲を制御しつつ Controller へ伝える役割を担っています。

UserCreateInteractor

<?php

namespace Packages\Application\User;

use Packages\Domain\CommonRepository\UuidGeneratorInterface;
use Packages\Domain\Models\User\AuthUserEntity;
use Packages\Domain\Models\User\UserEmail;
use Packages\Domain\Models\User\UserId;
use Packages\Domain\Models\User\UserName;
use Packages\Domain\Models\User\UserPassword;
use Packages\Domain\Models\User\UserRepository;
use Packages\UseCase\User\Create\UserCreateRequest;
use Packages\UseCase\User\Create\UserCreateResponse;
use Packages\UseCase\User\Create\UserCreateUseCaseInterface;

class UserCreateInteractor implements UserCreateUseCaseInterface
{
    /** @var UserRepository  */
    private $userRepository;

    /** @var UuidGeneratorInterface  */
    private $uuidGenerator;

    /**
     * UserCreateInteractor constructor.
     */
    public function __construct(
        UserRepository $userRepository,
        UuidGeneratorInterface $uuidGenerator
    ) {
        $this->userRepository = $userRepository;
        $this->uuidGenerator = $uuidGenerator;
    }

    public function __invoke(UserCreateRequest $request): UserCreateResponse
    {
        // Userのドメインモデルを生成
        // この時、全ての値の審査も行われる
        $user = new AuthUserEntity(
            UserId::create($this->uuidGenerator->generateUuidString()),
            UserName::create($request->getName()),
            UserEmail::create($request->getEmail()),
            UserPassword::create($request->getPassword())
        );

        // Repositoryに投げて永続化
        $user = $this->userRepository->create($user);

        // レスポンスとして返却する公開情報はResponseクラスで指定
        return new UserCreateResponse(
            $user->getId()->value(),
            $user->getName()->value(),
            $user->getEmail()->value()
        );
    }
}

「UserCreateUseCaseInterface」の具象クラスです。

ここで、いままで作成した Entity や ValueObject、Repository などの DomainObject 達をコントロールし、アプリケーションとしての振る舞いを実現し、利用者に機能を提供します。

利用者に公開するために Controller を作る

AuthController

<?php

namespace App\Http\Controllers;

use App\Http\Requests\Auth\AuthLoginRequest;
use App\Http\Requests\Auth\AuthRegisterRequest;
use Packages\UseCase\User\Create\UserCreateRequest;
use Packages\UseCase\User\Create\UserCreateUseCaseInterface;
use Packages\UseCase\User\Get\UserGetRequest;
use Packages\UseCase\User\Get\UserGetUseCaseInterface;
use Illuminate\Http\JsonResponse;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * @OA\Post(
     *     path="/api/auth/register",
     *     tags={"User/Auth"},
     *     description="ユーザー新規登録",
     *     @OA\RequestBody(
     *         required=true,
     *         @OA\MediaType(
     *             mediaType="application/x-www-form-urlencoded",
     *             @OA\Schema(
     *                 type="object",
     *                 @OA\Property(
     *                     property="name",
     *                     description="氏名",
     *                     type="string",
     *                     default="Ippei Kamimura"
     *                 ),
     *                 @OA\Property(
     *                     property="email",
     *                     description="メールアドレス",
     *                     type="string",
     *                     default="ippei_kamimura@icloud.com"
     *                 ),
     *                 @OA\Property(
     *                     property="password",
     *                     description="パスワード",
     *                     type="string",
     *                     default="aaaaaa"
     *                 ),
     *                 @OA\Property(
     *                     property="password_confirmation",
     *                     description="パスワード(確認)",
     *                     type="string",
     *                     default="aaaaaa"
     *                 )
     *             )
     *         )
     *     ),
     *     @OA\Response(
     *         response="200",
     *         description="認証トークンを返す",
     *     )
     * )
     */
    public function register(
        AuthRegisterRequest $request,
        UserCreateUseCaseInterface $userCreateUseCase
    ): JsonResponse {
        $userCreateRequest = new UserCreateRequest(
            $request->name,
            $request->email,
            $request->password
        );

        $response = $userCreateUseCase($userCreateRequest);

        if (!$token = auth('api')->attempt($request->all())) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

		return response()->json([
			'user' => $response->toArray(),
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    }
}

Laravel の Controller クラスです。

利用者からの Request を受け取り、UseCase へそのリクエストを処理できる形に変換して伝えることが役割です。

今回の実装では Presenter は取り上げていないので、UseCase からのレスポンスを JsonResponse に変換して利用者へレスポンスを返すような建て付けにしています。

AuthRegisterRequest

<?php

namespace App\Http\Requests\Auth;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Packages\Domain\Models\User\UserPassword;
use Packages\Domain\Models\User\UserName;

class AuthRegisterRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => [
                'required',
                'string',
                sprintf('min:%s', UserName::MIN_LNEGTH),
                'max:255'
            ],
            'email' => [
                'required',
                'string',
                'email:strict,dns',
                'max:255',
                'unique:users'
            ],
            'password' => [
                'required',
                'string',
                sprintf('min:%s', UserPassword::MIN_LENGTH),
                'confirmed',
            ],
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(
            response()->json([
                'message' => $validator->errors()->toArray(),
            ], 403)
        );
    }
}

Laravel の FormRequest クラスです。

通常の「Illuminate\Http\Request」クラスを継承しており、同時に validation 機能も提供してくれる便利なクラスなので使わない手はありません。

Controller はクリーンアーキテクチャーにおける同心円の中で一番外側の Frameworkds 層の住人なので、ここで Laravel 独自の技術に依存することは問題ではありません。

ここで、「ValueObject で値の審査をしているから FormRequest での Validation は不必要では?」という意見もあると思います。

たしかに、この 2 つは値に対するルールが同一になると思いますが、FormRequest と ValueObject ではそれぞれが持つ役割が違います。

ValueObject は値そのもののビジネスルールであり、FormRequest はそのビジネスルールに従い、Request を審査するという役割です。

FormRequest はルール以外の者の侵入を拒み、ValueObject はルール以外の生成を拒みます。

そして ValueObject が生成を拒むおかげで、Request 以外からのルートで Entity が生成される場合でも審査機能を利かすことができます。

この様に、「Laravel の機能を使わない」ではなく「ドメイン層やアプリケーション層では使わない」というルールに徹して活用すれば、ビジネスルールが特定の技術基盤に依存してしまうことを避けられます。

まとめ

ここまで、クリーンアーキテクチャーを Laravel で活用する方法を自分なりに研究してきましたが、簡潔に言うならば「クリーンアーキテクチャーとは、ビジネスルールの在り処を一箇所に集め、技術基盤との堺にインターフェースを挟むことでビジネスルールに対して"pluggable(脱着可能)"にする事で、"可変性"を保ちながらプロダクトを成長させるための指針」と言えると思います。

特にインターフェースの文脈で私が思うことは、「抽象に依存」という言葉が示すとおり我々開発者にとって物事を抽象化することが最も重要な仕事と言っても過言ではありません。

なぜならば、頭の中で抽象化出来ていない物事のインターフェースを作ることは出来ないからです。

抽象化出来ていないと、UI 層で仕様をもみながら行きあたりばったりでコードを書くことになり、結果として「賢い UI」になって行き、可変性の低いソースコードになって行く。

だから、いきなりエディターに向かうのではなく、ドメイン従事者に多くのヒアリングを行い、紙ベースで簡単にでも良いので全体構成を整理することから始めた方が良い。

開発者が関与するプロダクトの抽象度を上げ、質の高いインターフェースを作り上げることができるかどうかは、その Domain についてどれだけ知り尽くしているかに依存します。

そして、そのインターフェースの抽象度の質によって、そのプロダクトが将来に向けて拡張的で居られるかどうかが決まってしまう。

アーキテクチャーにゴールや正解はなく、常に思考を練り、よりスマートに、よりシンプルにバージョンアップしていかなくてはならないものです。

思考を凝らし、難しいことを単純化するツールとして、クリーンアーキテクチャーの考え方は活用の価値があると思います。

GitHubで編集を提案

Discussion