この記事は次の記事の続編に近い内容となっています。
もし可能であればこちらの記事を読み進める前に、次の記事をご覧いただくとより内容がわかりやすいでしょう。
Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22
はじめに
皆さんこんな図をご存知でしょうか。
The Clean Architecture: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
これはクリーンアーキテクチャというアイデアを表した図です。
同心円が特徴的な図ですね。
この同心円は、最も重要なビジネスロジックを中心に見据えることで外界の変化から防衛、対応していこうというコンセプトを表しています。
より具体的にいえば、ビジネスロジックは特定の技術に依存すべきでないということです。
つまり、ソフトウェアレベルで疎結合を達成しようとしています。
ビジネスに比べて UI やデータベース、フレームワークなどは移り変わりやすいものです。
そういった変化に対して、最も重要なビジネスロジックは本来影響を受けるべきではありません。
反対にビジネスロジックの変化は UI やデータベース、フレームワークに正しく影響させるべきです。
同心円の図は依存の方向を中心に向け、内側のレイヤーの変更は外側のレイヤーに伝播し、外側のレイヤーの変更は内側のレイヤー影響しないようにするというアイデアを表しています。
コンセプト自体は明快なクリーンアーキテクチャですが、図だけではとても抽象的に思えますね。
しかし、実はこの図、かなり詳細な実装まで落とし込むことができるのです。
それについては以下記事にて解説を行いました。
Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22
では、この記事は何なのかというと、「Laravelで実践クリーンアーキテクチャ(以降、以前の記事と称します)」で妥協した部分も実装に落とし込んでみよう、という内容です。
コード
以前の記事に加筆して作ったサンプルコードです。
https://github.com/nrslib/StrictLaraClean
妥協点
冒頭でお話したとおり、以前の記事では妥協していた部分があります。
この図の Presenter と UseCaseOutputPort の部分ですね。
こちらの図であれば Presenter と OutputBoundary です。
具体的なコードを確認してみましょう。
次のコードは MVC フレームワークのコントローラです。
class UserController extends BaseController
{
public function index(UserGetListUseCaseInterface $interactor)
{
$request = new UserGetListRequest(1, 10);
$response = $interactor->handle($request);
$users = array_map(
function ($x) {
return new UserViewModel($x->id, $x->name);
},
$response->users
);
$viewModel = new UserIndexViewModel($users);
return view('user.index', compact('viewModel'));
}
public function create(UserCreateUseCaseInterface $interactor, Request $request)
{
$name = $request->input('name');
$request = new UserCreateRequest($name);
$response = $interactor->handle($request);
$viewModel = new UserCreateViewModel($response->getCreatedUserId(), $name);
return view('user.create', compact('viewModel'));
}
}
いずれのアクションもリクエストからデータを処理してレスポンスを戻すという一般的な処理です。
しかし、この処理がすでにクリーンアーキテクチャの図からはかけ離れているのです。
このコードを図に表すと次のようになります。
オリジナルの図(次の図)と比べるとだいぶ形がちがいますね。
元々の図では Controller が UseCaseInputPort (コードでは UserCreateUseCaseInterface) を呼び出して、その後の処理は UseCaseInteractor に流れ、UseCaseOutputPort を経て Presenter へ処理が流れるようになっています。
もしもこの図を再現した場合、コードは次のようになるでしょう。
class UserController extends BaseController
{
public function index(UserGetListUseCaseInterface $interactor)
{
$request = new UserGetListRequest(1, 10);
$interactor->handle($request);
}
public function create(UserCreateUseCaseInterface $interactor, Request $request)
{
$name = $request->input('name');
$request = new UserCreateRequest($name);
$interactor->handle($request);
}
}
戻り値がなくなってしまいました。
これは通常の MVC フレームワークのコードとは大分異なるコードです。
だいぶ違和感を感じるのではないでしょうか。
通常 MVC フレームワークにおいて、このようなコードを書いてもうまくは動きません。
ですので以前の記事では妥協をすることにしたのですが(妥協してもクリーンな状態は保たれますし)、今回は敢えて妥協しないという方向で進んでいきます。
調査
というわけで妥協しないで魔改造実装するためにまずは調査をしていきましょう。
まずは最終形となる理想的なコードを確認します。
コントローラは次のコードを目指します。
class UserController extends BaseController
{
public function index(UserGetListUseCaseInterface $interactor)
{
$request = new UserGetListRequest(1, 10);
$interactor->handle($request);
}
public function create(UserCreateUseCaseInterface $interactor, Request $request)
{
$name = $request->input('name');
$request = new UserCreateRequest($name);
$interactor->handle($request);
}
}
続いて UseCase のコードは次のように結果を Presenter に伝えるようにします。
class UserCreateInteractor implements UserCreateUseCaseInterface
{
/**
* @var UserRepositoryInterface
*/
private $userRepository;
/**
* @var UserCreatePresenter
*/
private $presenter;
/**
* UserCreateInteractor constructor.
* @param UserRepositoryInterface $userRepository
* @param UserCreatePresenter $presenter
*/
public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenter $presenter)
{
$this->userRepository = $userRepository;
$this->presenter = $presenter;
}
/**
* @param UserCreateRequest $request
* @return void
*/
public function handle(UserCreateRequest $request)
{
$userId = new UserId(uniqid());
$userName = $request->getName();
$createdUser = new User($userId, $userName);
$this->userRepository->save($createdUser);
$response = new UserCreateResponse($userId->getValue(), $userName);
$this->presenter->output($response);
}
}
そして Presenter はどのような実装になるかわかりませんが UseCase の結果を表示用に整形し、どうにかして表示する仕組みへの通知を行うことを目指します。
class UserCreatePresenter {
public function output(UserCreateResponse $outputData)
{
$viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
// ここでどうにかして表示できるように通知する
}
}
この流れを整理すると次の順序で処理が行われます。
- Controller が InputPort (UserCreateUseCaseInterface) を呼び出す
- InputPort の実装である UserCreateInteractor に処理が移譲される
- UserCreateInteractor は Presenter (UserCreateUseCasePresenter) に結果を伝える
- Presenter の実装である UseCreatePresenter に処理が移譲される
これであれば次の図の再現になっているのではないでしょうか。
それでは調査していきましょう。
なにはともあれ、まずは Controller が戻り値を返却しなくてもデータを表示できるか確認します。
エントリポイントの public\index.php を確認してみると次のコードがあります。
/*
* 省略
*/
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
ここを見る限りだと$kernel->handle
がレスポンスを戻り値として返却していて、そのレスポンスのsend
メソッドを呼ぶことで処理がうまくいきそうです。
であれば $response にあたるものを戻り値以外の方法で手に入れれば何とかなりそうな気がしてきました。
次は$kernel
がどうやってレスポンスを生成しているか探してみます。
$kernel
はコードを見てわかるとおり Illuminate\Contracts\Http\Kernel から生成されています。
この Kernel は interface ですので、実装しているクラスを検索してみましょう。
すると Illuminate\Foundation\Http\Kernel が見つかり、更にそれを継承した App\Http\Kernel というクラスも見つかりました。
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
];
/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}
Middleware の戻り値がどんなものか気になります。
ためしに独自の Middleware を作ってみて var_dump してみます。
Middleware は次のコマンドでスケルトンを生成できます。
$ php artisan make:middleware CleanArchitectureMiddleware
できあがったスケルトンに var_dump を差し込みましょう。
class CleanArchitectureMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
var_dump($response);
return $response;
}
}
そして忘れずに Kernel に登録もしておきます。
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array
*/
protected $middleware = [
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
CleanArchitectureMiddleware::class // ← ここに追加
];
この状態でコードを実行すると Illuminate\Http\Response というものがやり取りされていて、その中に Http のコンテントや Http ステータスコードなどが存在しているのがわかります。
ためしにコントローラの戻り値をなくしてみるとどうなるでしょうか。
class UserController extends BaseController
{
public function index(UserGetListUseCaseInterface $interactor)
{
$request = new UserGetListRequest(1, 10);
$response = $interactor->handle($request);
$users = array_map(
function ($x) {
return new UserViewModel($x->id, $x->name);
},
$response->users
);
$viewModel = new UserIndexViewModel($users);
// return view('user.index', compact('viewModel')); // ← 試しにコメントアウト
}
すると先ほど存在していたコンテントやステータスコードが現れなくなりました。
コントローラの戻り値がここに利用されているのは確定のようです。
リクエストとレスポンスの流れがわかったところで、今度はコントローラの戻り値の具体的な生成方法を確認してみましょう。
コントローラの戻り値はview()
という関数の戻り値ですので、view()
の実装を探してみると helpers.php のコードが見つかります。
if (! function_exists('view')) {
/**
* Get the evaluated view contents for the given view.
*
* @param string $view
* @param array $data
* @param array $mergeData
* @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory
*/
function view($view = null, $data = [], $mergeData = [])
{
$factory = app(ViewFactory::class);
if (func_num_args() === 0) {
return $factory;
}
return $factory->make($view, $data, $mergeData);
}
}
view
関数はどこでも呼ぶことができそうなので、Presenter で呼びだすことができます。
また具体的な処理を眺めた感じもビューをうまく生成してくれそうな感じにみえます。
実装
というわけでいざ実装をしてみましょう。
Middleware
先ほど作成した Middleware に view 関数の結果を格納するフィールドを用意しておきます。
class CleanArchitectureMiddleware
{
public static $view; // view 関数の結果を格納するフィールド
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
return $response;
}
}
正直な話この格納場所はどこでもよかったのですが、ここがわかりやすそうなのでそのまま使います。
index.php
次にエントリポイントを魔改造改修します。
具体的には Middleware に用意した view 関数の結果を利用するようにします。
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
$kernelResponse = $kernel->handle(
$request = Illuminate\Http\Request::capture()
);
$view = \App\Http\Middleware\CleanArchitectureMiddleware::$view; // 格納された view 関数の結果を取り出して
$response = $view !== null // データが存在してたら
? new \Symfony\Component\HttpFoundation\Response($view) // そのデータをレスポンスに
: $kernelResponse; // さもなければ通常のレスポンスを利用
$response->send();
$kernel->terminate($request, $response);
これで戻り値を返却する以外の方法でレスポンスをつくることができそうです。
これでコア部分の魔改造ができたので次節からは次の図を準備していきます。
UseCaseOutputPort (OutputBoundary)
UseCaseOutputPort (OutputBoundary) は<I>というマークがついているとおり interface です。
UseCaseInteractor が呼び出します。
図にもあるとおり OutputData と呼ばれる表示出力用のデータ構造体が引き渡されます。
/**
* Interface UserCreatePresenterInterface
* @package packages\UseCase\User\Create
*/
interface UserCreatePresenterInterface
{
/**
* @param UserCreateResponse $outputData
* @return void
*/
public function output(UserCreateResponse $outputData);
}
これが interface で用意されているのは後述の Presenter という表示出力のバリエーションを増やせるようにするためです。
Presenter
Presenter は UseCaseOutputPort (OutputBoundary) を実装し、UseCaseInteractor が生成した OutputData をそれぞれのビュー用に変換する作業を行います。
class UserCreatePresenter implements UserCreatePresenterInterface
{
public function output(UserCreateResponse $outputData)
{
$viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
CleanArchitectureMiddleware::$view = view('user.create', compact('viewModel'));
}
}
元となる OutputData が同じであってもビューが異なれば表示用のデータは異なるということですね。
具体例としては、たとえばテスト用にどのようなデータが得られたかを確認する場合は次のような Presenter を実装することで結果を取得するオブジェクトを用意することができます。
class TestUserCreatePresenter implements UserCreatePresenterInterface {
public $id;
public $name;
/**
* @param UserCreateResponse $outputData
* @return void
*/
public function output(UserCreateResponse $outputData)
{
$this->id = $outputData->getCreatedUserId();
$this->name = $outputData->getUserName();
}
}
Middleware をいちいち通す必要がなくなるので気軽に処理結果を得ることができます。
$presenter = new TestUserCreatePresenter();
$repository = new InMemoryUserRepository();
$interactor = new UserCreateInteractor($repository, $presenter);
var_dump($presenter->id);
UseCaseInteractor
UseCaseInteractor はもはや戻り値を戻さず、UseCaseOutputPort (OutputBoundary) に OutputData を伝えるだけになります。
class UserCreateInteractor implements UserCreateUseCaseInterface
{
/**
* @var UserRepositoryInterface
*/
private $userRepository;
/**
* @var UserCreatePresenter
*/
private $presenter;
/**
* UserCreateInteractor constructor.
* @param UserRepositoryInterface $userRepository
* @param UserCreatePresenterInterface $presenter
*/
public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenterInterface $presenter)
{
$this->userRepository = $userRepository;
$this->presenter = $presenter;
}
/**
* @param UserCreateRequest $request
* @return void
*/
public function handle(UserCreateRequest $request)
{
$userId = new UserId(uniqid());
$userName = $request->getName();
$createdUser = new User($userId, $userName);
$this->userRepository->save($createdUser);
$response = new UserCreateResponse($userId->getValue(), $userName);
$this->presenter->output($response);
}
}
Controller
Controller は入力情報を UseCaseInputPort (InputBoundary) が要求するデータ (InputData) に適合させることに集中することになります。
class UserController extends BaseController
{
public function index(UserGetListUseCaseInterface $interactor)
{
$request = new UserGetListRequest(1, 10);
$interactor->handle($request);
}
public function create(UserCreateUseCaseInterface $interactor, Request $request)
{
$name = $request->input('name');
$request = new UserCreateRequest($name);
$interactor->handle($request);
}
}
戻り値がなくなり、一般的な MVC フレームワークのコントローラとはかけ離れたものになりました。
これでクリーンアーキテクチャの図を完全に再現した Web アプリケーションの形が完成です。
処理の流れ
ためしにユーザを生成するときの処理の流れを追ってみましょう。
まず入力データはUserController
に引き渡され、UserCreateUseCaseInterface
(UseCaseInputPort, InputBoundary) の処理が呼び出されます。
class UserController extends BaseController
{
public function create(UserCreateUseCaseInterface $interactor, Request $request)
{
$name = $request->input('name');
$request = new UserCreateRequest($name);
$interactor->handle($request);
}
}
UserCreateUseCaseInterface
の処理が呼び出されるとその実装クラスUserCreateInteractor
(UseCaseInteractor) に処理が移譲されます。
このオブジェクトが処理した結果はUserCreatePresenterInterface
(UseCaseOutputPort, OutputBoundary) に通知されます。
class UserCreateInteractor implements UserCreateUseCaseInterface
{
private $userRepository;
private $presenter;
public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenterInterface $presenter)
{
$this->userRepository = $userRepository;
$this->presenter = $presenter;
}
public function handle(UserCreateRequest $request)
{
$userId = new UserId(uniqid());
$userName = $request->getName();
$createdUser = new User($userId, $userName);
$this->userRepository->save($createdUser);
$response = new UserCreateResponse($userId->getValue(), $userName);
$this->presenter->output($response);
}
}
UserCreatePresenterInterface
を実装するUserCreatePresenter
(Presenter) に処理が移譲され、UserCreatePresenter
は表示用にデータを整形し、「どうにかして」表示できるように通知を行います。
class UserCreatePresenter implements UserCreatePresenterInterface
{
public function output(UserCreateResponse $outputData)
{
$viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName());
CleanArchitectureMiddleware::$view = view('user.create', compact('viewModel'));
}
}
Flow of control (処理の流れ)と照らし合わせると一致することがわかるでしょうか。
で、なにが嬉しいの?
今回のコードは MVC フレームワークの常識から逸脱したコードになっています。
正直な気持ちとして「これが最高だからこれで作ろうな!」と手放しに推せるコードではないです。
それでもここまでコードを書いて、なぜ Presenter のようなまだるっこしいものを採用しているのかを見出すことができた気がしたので、それを書いてみます。
GUI のプログラムのパターンに古典的 MVC (Web の MVC とは異なるので注意)というものがあります。
図にすると次のイメージになります。
古典的 MVC ではユーザの操作は Controller に伝えられ、Controller はその入力から Model の処理を呼び出します。Model の処理結果は View に通知され、View がそれを描画することでユーザに伝えられます。
このパターンにおいて、着目されるのは責務を分けることになりがちです。
しかし、それ以外にも大事な要素があって、それは処理の方向だったりします。
なにかをするとき、臨機応変な対応が求められるのと決まりきった手順が決まっているのでは後者の方が簡単ですよね。
プログラムの処理の流れも場合によってはあっちへこっちへいくようなプログラムよりも、常に一方向であった方が理解しやすいものです。
つまり、古典的 MVC は複雑になりがちな GUI アプリケーションにおいて、責務を分散し、処理方向を一方向にすることで理解しやすいソフトウェアを作ろうというパターンだったりします。
さて、ここで改めてクリーンアーキテクチャの Flow of Control を確認してみます。
Presenter を用意することで戻り値を用意する必要がなくなりました。
結果として処理の方向は一方向に固定されます。
処理の流れが一方向ということは理解しやすいはずです。
そう考えると、戻り値を戻す従来の形よりも、今回実装した構成の方がシンプルなソフトウェアではないでしょうか。
処理を一方向に固定することに意義がある。そういった意向がここにあるのではないのかと感じた次第でした。
まとめ
正直な話、今回のコードを実装しながら思っていたのは「物議を醸しだしそうなコードだな」でした。
もちろんこれこそが正しいコードである、とは考えておりません。
封印することも考えたのですが、アイデアとしては面白く感じたため、今回の記事を投稿するに至りました。
この記事でお話したとおりクリーンアーキテクチャの構成を MVC フレームワークにあてはめてみるとかなり違和感を感じます。
MVC フレームワークの大前提を覆すようなコードになっていると言っても過言ではないと思います。
しかし Controller をゲームのコントローラとして見立ててみると案外素直に受け入れられる可能性もなくはないと感じました。
ゲームのコントローラはボタンを押した結果をユーザに伝えたりはしません。(振動機能などはありますが)
押した結果をユーザに伝えるのは View であるモニタの役目です。
こう考えると Controller は戻り値など返さず、入力された情報だけに集中する方が自然ではないのでしょうか。
クリーンアーキテクチャの目標のひとつは特定の技術からの独立です。
それがアプリケーションの防衛に繋がるからです。
特定の技術に依存した形でソフトウェアを開発することはある種の危険性をはらみます。
もしもフレームワークが廃れ、別のフレームワークに乗せ換えることになったらどうなるか。
データ永続化装置がアーキテクチャレベルで変更することになったらどうすればよいのか。
こういったリスクから距離を取るための手法として、クリーンアーキテクチャはよい選択肢になるのではないでしょうか。