Developers building Laravel applications are being urged to ditch the new keyword inside their controllers and switch to the Factory Method pattern. By moving object creation into dedicated factories, code becomes easier to extend, test, and maintain—benefits that matter as projects grow beyond a handful of endpoints.
なぜ new キーワードが隠れたリスクになるのか
コントローラーに次のような行が含まれている場合、
$processor = new StripePaymentProcessor($config);
そのコントローラーは StripePaymentProcessor クラスと密結合してしまいます。決済プロセッサを必要とするすべての場所でその行が繰り返され、生成ロジックがコードベース全体に散らばってしまいます。もし後になって、プロセッサにロガーやキャッシュマネージャー、あるいは異なる設定形式が必要になった場合、それらすべての箇所を更新しなければなりません。その結果、変更に弱く、ユニットテストでのモック化も困難な、脆弱なコードになってしまいます。
Factory Methodパターンによる解決
Factory Methodパターンは、直接的なインスタンス化を、特定のインターフェースのインスタンスを返すメソッドに置き換えます。コントローラーはインターフェースに依存し、一方でファクトリクラスは、どの具象クラスを構築すべきか、およびその依存関係をどのように組み立てるかを知っています。すべての生成ロジックが一箇所に集約されるため、新しい依存関係の追加や実装の切り替えは、ファクトリのみを変更すれば済みます。
主な利点
- 疎結合 – コントローラーは具象クラスではなく、抽象に対して動作します。
- 構築の集約化 – 構築プロセスを変更する場合、単一のファイルを編集するだけで済みます。
- テスト容易性 – ファクトリはスタブに置き換えたり、モックを使用したりできるため、コントローラーを分離してテストできます。
- 開放閉鎖の原則 (Open/Closed Principle) – 既存のコントローラーコードに手を加えることなく、新しい機能(例:新しい決済ゲートウェイ)を追加できます。
コーヒーショップの例え
すべての注文に対して、長い if-else 文の羅列を使い、豆を挽き、ミルクをスチームし、お湯を注ぐ作業を手動で行わなければならないバリスタを想像してみてください。もしラテのレシピが変わったら、すべてのバリスタがその手順を学び直さなければなりません。注文の種類を受け取り、内部で準備を行うコーヒーマシンがあれば、この問題は解決します。バリスタはマシンに必要なものを伝えるだけでよく、マシンがすべての手順をカプセル化します。レシピを更新する場合、すべてのバリスタではなく、マシンを調整するだけで済みます。
Laravelはすでにファクトリを活用しています
Laravel自体のコアコンポーネントが、このパターンの実際の活用例を示しています。
- Database –
ConnectionFactoryが、MySQL または PostgreSQL の接続を構築するかどうかを決定します。 - Queues –
QueueManagerが Redis、SQS、またはその他のバックエンド用のドライバーを作成します。 - Filesystem –
FilesystemManagerがローカルまたは S3 のディスクインスタンスを生成します。 - Mail –
MailManagerが SMTP、Mailgun、またはその他のメールドライバーを解決します。
フレームワークがこれらの重要なサービスにファクトリを信頼して使用しているなら、カスタムコードもそれに倣うべきです。
ファクトリを導入すべき時
次のような場合はファクトリを使用してください:
- オブジェクトの生成に、複数の設定ステップや外部サービスが関与する場合。
- 相互に入れ替え可能な実装が複数存在する場合(異なる決済ゲートウェイ、ストレージプロバイダーなど)。
- 実装可能なリストが増えることが予想される場合。
次のような場合はファクトリを避けてください:
- 構築が、追加の設定を必要としない、単一の不変な
new呼び出しである場合。 - 実装が常に1つだけで、余分な抽象化が不要なオーバーヘッドになる場合。
クイックウォークスルー:決済コントローラーのリファクタリング
- インターフェースを定義する –
process(array $data)メソッドを持つPaymentProcessorInterfaceを作成します。 - 具象クラスを実装する – インターフェースを満たす
StripePaymentProcessorやPayPalPaymentProcessorを作成します。 - ファクトリを作成する –
make(string $driver): PaymentProcessorInterfaceメソッドを持つPaymentProcessorFactoryを作成します。内部では、switch文やマップを使用して適切なクラスを返し、Laravel のサービスコンテナから必要なサービスを注入します。 - ファクトリを注入する – コントローラーのコンストラクタで
PaymentProcessorFactoryを型ヒントします。Laravel が自動的に解決します。 - ファクトリを使用する –
$processor = $this->processorFactory->make('stripe'); $processor->process($request->all());
これで、コントローラー内で new や具象プロセッサに言及することはありません。新しいゲートウェイを追加するには、クラスを作成してファクトリのマップを拡張するだけで済み、コントローラーを変更する必要はありません。
潜在的なデメリットとその軽減策
ファクトリに対する主な批判は、間接性が増すことであり、些細なオブジェクトに対しては冗長に感じられることがあります。単純なサービスをオーバーエンジニアリングすると、実質的なメリットがないままコードベースを肥大化させる可能性があります。重要なのは、構築の複雑さを評価することです。もしオブジェクトが依存関係のない単なるデータ保持者であれば、直接 new を使用しても問題ないでしょう。また、ファクトリが関連のないロジックの掃き溜めになってしまうこともあります。ファクトリの役割をオブジェクトの生成に集中させ、設定は専用のサービスプロバイダーに委譲することで、明快さを保つことができます。
次に注意すべき点
- サービスコンテナのバインディング – サービスプロバイダーでインターフェースをファクトリクラスにバインドしておけば、Laravelのコンテナが自動的にファクトリを解決できます。
- オートディスカバリ – 一部のパッケージは独自のファクトリを公開しているため、名前の衝突に注意してください。
- テスト戦略 – コントローラーのユニットテストを行う際は、ファクトリをスタブ化されたプロセッサを返すモックに置き換えることで、テストの高速性と独立性を確保してください。
まとめ
点在する new 呼び出しを適切に配置されたファクトリに置き換えることで、構築処理を中央集約化し、結合度を下げ、カスタムコードをLaravel自身の設計思想に合わせることができます。新しい決済プロバイダー、ストレージバックエンド、あるいはプラグイン形式のコンポーネントなど、将来的な拡張を見込んでいるチームにとって、Factory Method patternは、柔軟性と確信という形で大きな見返りをもたらす、低コストな投資となります。未来の自分自身、そしてそのコードを引き継ぐ誰もが、あなたに感謝することでしょう。
