ファットコントローラーとモデル:YiiやLaravelなどのMVCフレームワークに基づくほとんどの大規模プロジェクトにとって避けられない問題。コントローラとモデルを肥大化させる主なものは アクティブレコード 、そのようなフレームワークの強力で不可欠なコンポーネント。
Active Recordはアーキテクチャパターンであり、データベース内のデータにアクセスするためのアプローチです。 2003年の本でマーティンファウラーによって名付けられました エンタープライズアプリケーションアーキテクチャのパターン で広く使用されています PHP フレームワーク。
これは非常に必要なアプローチであるという事実にもかかわらず、アクティブレコード(AR)パターンは、ARモデルが次の理由で単一責任原則(SRP)に違反しています。
このSRPの違反は、アプリケーションプロトタイプをできるだけ早く作成する必要がある場合、迅速な開発とのトレードオフになりますが、アプリケーションが中規模または大規模なプロジェクトに成長する場合は非常に有害です。 「神」モデルとファットコントローラーはテストと保守が難しく、コントローラーのどこでもモデルを自由に使用すると、データベース構造を必然的に変更する必要がある場合に非常に困難になります。
解決策は簡単です。ActiveRecordの責任をいくつかのレイヤーに分割し、レイヤー間の依存関係を注入します。このアプローチでは、現在テストされていないレイヤーをモックできるため、テストも簡素化されます。
「ファット」なPHPMVCアプリケーションには、どこにでも依存関係があり、連動してエラーが発生しやすくなりますが、階層構造では依存性注入を使用して、物事をクリーンで明確に保ちます。
ここで取り上げる主要なレイヤーは5つあります。
階層構造を実装するには、 依存性注入コンテナ 、オブジェクトをインスタンス化して構成する方法を知っているオブジェクト。フレームワークがすべての魔法を処理するため、クラスを作成する必要はありません。次のことを考慮してください。
class SiteController extends IlluminateRoutingController { protected $userService; public function __construct(UserService $userService) { $this->userService = $userService; } public function showUserProfile(Request $request) { $user = $this->userService->getUser($request->id); return view('user.profile', compact('user')); } } class UserService { protected $userRepository; public function __construct(UserRepository $userRepository) { $this->userRepository = $userRepository; } public function getUser($id) { $user = $this->userRepository->getUserById($id); $this->userRepository->logSession($user); return $user; } } class UserRepository { protected $userModel, $logModel; public function __construct(User $user, Log $log) { $this->userModel = $user; $this->logModel = $log; } public function getUserById($id) { return $this->userModel->findOrFail($id); } public function logSession($user) { $this->logModel->user = $user->id; $this->logModel->save(); } }
上記の例では、UserService
SiteController
、UserRepository
に注入されますUserService
に注入されますおよびARモデルUser
およびLogs
UserRepository
に注入されますクラス。このコンテナコードはかなり単純なので、レイヤーについて説明しましょう。
LaravelやYiiなどの最新のMVCフレームワークは、従来のコントローラーの課題の多くを引き受けます。入力検証とプレフィルターは、アプリケーションの別の部分に移動されます(Laravelでは、これはいわゆる ミドルウェア 一方、Yiiではそれは 動作 )ルーティングとHTTP動詞ルールはフレームワークによって処理されます。これにより、プログラマーがコントローラーにコーディングする機能が非常に狭くなります。
コントローラの本質は、リクエストを取得して結果を提供することです。コントローラには、アプリケーションのビジネスロジックを含めるべきではありません。そうしないと、コードを再利用したり、アプリケーションの通信方法を変更したりすることが困難になります。たとえば、ビューをレンダリングする代わりにAPIを作成する必要があり、コントローラーにロジックが含まれていない場合は、データを返す方法を変更するだけで、準備は完了です。
この薄いコントローラーレイヤーはプログラマーを混乱させることがよくあります。コントローラーはデフォルトレイヤーであり、最上位のエントリポイントであるため、多くの開発者は、アーキテクチャについて何も考えずに、コントローラーに新しいコードを追加し続けます。その結果、次のような過度の責任が追加されます。
過剰に設計されたコントローラーの例を考えてみましょう。
//A bad example of a controller public function user(Request $request) { $user = User::where('id', '=', $request->id) ->leftjoin('posts', function ($join) { $join->on('posts.user_id', '=', 'user.id') ->where('posts.status', '=', Post::STATUS_APPROVED); }) ->first(); if (!empty($user)) { $user->last_login = date('Y-m-d H:i:s'); } else { $user = new User(); $user->is_new = true; $user->save(); } return view('user.index', compact('user')); }
この例が悪いのはなぜですか?多くの理由で:
last_login
の名前を変更するなど、データベース内の何かを変更した場合フィールドでは、すべてのコントローラーで変更する必要があります。コントローラは薄くする必要があります。実際、それがすべきことは、リクエストを受け取って結果を返すことだけです。これが良い例です:
//A good example of a controller public function user (Request $request) { $user = $this->userService->getUserById($request->id); return view('user.index', compact('user')); }
しかし、他のすべてのものはどこに行きますか?それはに属します サービスレイヤー 。
オンラインで無料でcプログラミングを学ぶ
サービス層はビジネスロジックの層です。ここで、そしてここでのみ、ビジネスプロセスフローとビジネスモデル間の相互作用に関する情報を配置する必要があります。これは抽象層であり、アプリケーションごとに異なりますが、一般的な原則は、データソース(コントローラーの責任)およびデータストレージ(下位層の責任)からの独立性です。
これは、成長の問題が発生する可能性が最も高い段階です。多くの場合、Active Recordモデルはコントローラーに返されます。その結果、ビュー(またはAPI応答の場合はコントローラー)はモデルと連携し、その属性と依存関係を認識している必要があります。これは物事を厄介にします。 Active Recordモデルのリレーションまたは属性を変更する場合は、すべてのビューとコントローラーのどこでも変更する必要があります。
ビューで使用されているActiveRecordモデルに出くわす可能性のある一般的な例を次に示します。
@foreach($user->posts as $post) - {{$post->title}}
@endforeach
簡単そうに見えますが、名前を変更するとfirst_name
フィールド、突然、このモデルのフィールドを使用するすべてのビューを変更する必要があります。これはエラーが発生しやすいプロセスです。この難問を回避する最も簡単な方法は、データ転送オブジェクト(DTO)を使用することです。
サービスレイヤーからのデータは、単純な不変オブジェクトにラップする必要があります。つまり、作成後に変更することはできません。そのため、DTOのセッターは必要ありません。さらに、DTOクラスは独立している必要があり、ActiveRecordモデルを拡張しないでください。ただし、注意が必要です。ビジネスモデルは必ずしもARモデルと同じではありません。
食料品の配達アプリケーションを考えてみましょう。論理的には、食料品店の注文には配達情報を含める必要がありますが、データベースには注文を保存してユーザーにリンクし、ユーザーは配達先住所にリンクします。この場合、複数のARモデルがありますが、上位層はそれらについて認識してはなりません。 DTOクラスには、注文だけでなく、配送情報やビジネスモデルに沿ったその他のパーツも含まれます。このビジネスモデルに関連するARモデルを変更する場合(たとえば、配信情報を注文テーブルに移動する場合)、コード内のあらゆる場所でARモデルフィールドの使用法を変更するのではなく、DTOオブジェクトのフィールドマッピングのみを変更します。
DTOアプローチを採用することで、コントローラーまたはビューでActiveRecordモデルを変更したいという誘惑を取り除きます。次に、DTOアプローチは、物理データストレージと抽象的なビジネスモデルの論理表現との間の接続の問題を解決します。データベースレベルで何かを変更する必要がある場合、その変更はコントローラーやビューではなくDTOオブジェクトに影響します。パターンを見ていますか?
簡単なDTOを見てみましょう。
//Example of simple DTO class. You can add any logic of conversion from an Active Record object to business model here class DTO { private $entity; public static function make($model) { return new self($model); } public function __construct($model) { $this->entity = (object) $model->toArray(); } public function __get($name) { return $this->entity->{$name}; } }
新しいDTOの使用も同様に簡単です。
//usage example public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); return view('user.index', compact('user')); }
ビューロジックを分離するために(ステータスに基づいてボタンの色を選択するなど)、デコレータの追加レイヤーを使用することは理にかなっています。 A デコレータ は、カスタムメソッドでラップすることにより、コアオブジェクトの装飾を可能にするデザインパターンです。これは通常、ビュー内でやや特殊なロジックで発生します。
あなた自身のグーグルグラスを作る方法
DTOオブジェクトはデコレータのジョブを実行できますが、実際には、日付の書式設定などの一般的なアクションに対してのみ機能します。 DTOはビジネスモデルを表す必要がありますが、デコレータは特定のページのデータをHTMLで装飾します。
デコレータを使用しないユーザープロファイルステータスアイコンのスニペットを見てみましょう。
@if($user->status == AppModelsUser::STATUS_ONLINE) Online @else Offline @endif {{date('F j, Y', strtotime($user->lastOnline))}}
この例は単純ですが、開発者がより複雑なロジックに迷うのは簡単です。これは、HTMLの読みやすさをクリーンアップするためのデコレータの出番です。ステータスアイコンスニペットを完全なデコレータクラスに拡張しましょう。
class UserProfileDecorator { private $entity; public static function decorate($model) { return new self($model); } public function __construct($model) { $this->entity = $model; } public function __get($name) { $methodName = 'get' . $name; if (method_exists(self::class, $methodName)) { return $this->$methodName(); } else { return $this->entity->{$name}; } } public function __call($name, $arguments) { return $this->entity->$name($arguments); } public function getStatus() { if($this->entity->status == AppModelsUser::STATUS_ONLINE) { return 'Online'; } else { return 'Offline'; } } public function getLastOnline() { return date('F j, Y', strtotime($this->entity->lastOnline)); } }
デコレータの使用は簡単です。
public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); $user = UserProfileDecorator::decorate($user); return view('user.index', compact('user')); }
これで、条件やロジックなしでビューでモデル属性を使用できるようになり、はるかに読みやすくなりました。
{{$user->status}} {{$user->lastOnline}}
デコレータを組み合わせることもできます。
public function user (Request $request) { $user = $this->userService->getUserById($request->id); $user = DTO::make($user); $user = UserDecorator::decorate($user); $user = UserProfileDecorator::decorate($user); return view('user.index', compact('user')); }
各デコレータはそれぞれの役割を果たし、独自の部分のみを装飾します。複数のデコレータのこの再帰的な埋め込みにより、追加のクラスを導入することなく、それらの機能を動的に組み合わせることができます。
リポジトリ層は、データストレージの具体的な実装で機能します。柔軟性と簡単な交換のために、インターフェースを介してリポジトリを注入するのが最善です。データストレージを変更する場合は、リポジトリインターフェースを実装する新しいリポジトリを作成する必要がありますが、少なくとも他のレイヤーを変更する必要はありません。
リポジトリはクエリオブジェクトの役割を果たします。リポジトリはデータベースからデータを取得し、いくつかのActiveRecordモデルの作業を実行します。このコンテキストでは、アクティブレコードモデルは、単一のデータモデルエンティティ(情報をモデル化して保存する必要のあるシステム内のオブジェクト)の役割を果たします。各エンティティには情報が含まれていますが、それがどのように表示されたか(データベースから作成または取得されたかどうか)、または独自の状態を保存および変更する方法はわかりません。リポジトリの責任は、エンティティを保存および/または更新することです。これにより、リポジトリ内のエンティティの管理が維持され、エンティティが単純になるため、関心の分離が向上します。
データベースとActiveRecordの関係に関する知識を使用してクエリを作成するリポジトリメソッドの簡単な例を次に示します。
public function getUsers() { return User::leftjoin('posts', function ($join) { $join->on('posts.user_id', '=', 'user.id') ->where('posts.status', '=', Post::STATUS_APPROVED); }) ->leftjoin('orders', 'orders.user_id', '=', 'user.id') ->where('user.status', '=', User::STATUS_ACTIVE) ->where('orders.price', '>', 100) ->orderBy('orders.date') ->with('info') ->get(); }
新しく作成されたアプリケーションでは、コントローラー、モデル、およびビューのフォルダーのみが見つかります。 YiiもLaravelも、サンプルアプリケーションの構造にレイヤーを追加していません。初心者でも簡単で直感的なMVC構造により、フレームワークでの作業が簡素化されますが、サンプルアプリケーションが例であることを理解することが重要です。これは標準でもスタイルでもありません。また、アプリケーションアーキテクチャに関する規則を課すこともありません。タスクを個別の単一責任レイヤーに分割することで、保守が容易な柔軟で拡張可能なアーキテクチャーを実現します。覚えておいてください:
したがって、複雑なプロジェクトや将来成長する可能性のあるプロジェクトを開始する場合は、責任をコントローラー、サービス、およびリポジトリの各レイヤーに明確に分割することを検討してください。