1ヶ月ほど前に、「Java言語で学ぶデザインパターン」という本を読みました。設計におけるデザインパターンの典型例が23個紹介されている本で、FactoryクラスやBuilderクラスの意図を理解するのにとても役立ちました。
そしてさっそく実務で使う機会があったので、復習も兼ねて簡単にまとめておきます。
要件例
ECサイト開発などでは、決済機能が必要になることが多いです。そして複数の支払い方法(代引き、領収書払い)に対応する場合、支払い方法ごとの処理を実装しなければなりません。
今回の例として、下記の3種類の支払い方法がある決済処理を考えてみます。
・代引き
・クレジットカード
・領収書払い
実装イメージ
フロントから支払い方法のパラメーターが渡ってくると仮定して、愚直にコントローラーにガリガリ書いていくとこんな感じになります。
public function Payment(Request $request)
{
if ($request->get('pay_way') === config('pay_way.collect_on_delivery')) {
// 代引きの場合の処理
} else if ($request->get('pay_way') === config('pay_way.credit_card')) {
// クレジットカードの場合の処理
} else {
// 領収書払いの場合の処理
}
もうこの時点でイヤな予感しかしませんね。ただでさえ複雑&長い処理になるであろう決済の処理を、コントローラーに全部記述するとあっという間に地獄の出来上がりです。一つのアクションメソッドで数百行も夢ではありません。
それぞれの決済方法ごとのサービスクラスを作成し、具体的な処理はそこで行われるようにしたいところです。だとすると、こんな感じになるでしょうか...?
public function Payment(Request $request)
{
if ($request->get('pay_way' === config('pay_way.collect_on_delivery')) {
$collectOnDelivery = new CollectOnDelivery($request);
} else if ($request->get('pay_way') === config('pay_way.credit_card')) {
$creditCard = new creditCard($request);
} else {
$bill = new Bill($request);
}
// 決済方法ごとの処理が続く
}
クラスを作成し、処理をその中に閉じ込めることでコントローラー側の記述量はある程度少なくなることが予想されます。
とはいえ、まだ冗長的な感じがしますし、if文などのネストが深くなって可読性が悪くなる可能性も十分考えられます。
各クラス生成箇所も、今は3種類だけなのでこの量で収まっていますが、たとえば必要なクラスが増えた場合や、クラス生成時にある処理を挟みたい場合など(ログ出力とか)があると、どんどん行数が増え、可読性が低下します。
・クラス生成処理
・後半の決済処理
この2つをコントローラー内でスマートに完結させたいところです。
Factoryパターンでクラス生成をすっきりさせる
まず、前半部のクラス生成処理に対して、Factoryパターンの適用を考えてみます。PaymentFactoryというクラスを作成し、以下のように書きます。
<?php
namespace App\Services;
class PaymentFactory
{
public static function create()
{
$classMap = [
config('pay_way.collect_on_delivery') => 'PaidOnDelivery',
config('pay_way.credit_card') => 'CreditCard',
config('pay_way.bill') => 'Bill',
];
$klass = sprintf('App\Services\%s', \Arr::get($classMap, request()->get('pay_way')));
if (!class_exists($klass)) {
throw new \Exception('invalid payway');
}
return new $klass();
}
}
request->get('pay_way')の中身によって、返却されるインスタンスが変わるようになっています。
クラス生成処理をPaymentFactoryの中に閉じ込めたことによって、コントローラー側は以下のように書けるようになりました。
public function Payment(Request $request)
{
try {
$payment = PaymentFactory::create();
} catch (\Exception $e) {
// Error Handling
}
}
これなら、もしクラスの生成処理が変更になってもコントローラー側に手を加えず、PaymentFactory内の変更だけで完結できます。多少煩雑な処理が必要になっても、PaymentFactory内に新しいメソッドを生やしたり、新しくクラスを作って対処できるはずです。
Facadeパターンで決済処理の記述も簡潔に
コントローラーの前半部分の見通しはよくなりましたが、決済処理を行う後半部分はまだ複雑になる可能性を潜んでいます。
たとえば、決済クラスにさまざまなメソッドを自由に実装すると、結局以下のようにif文で処理を分岐させるしかなくなります。
public function Payment(Request $request)
{
try {
$payment = PaymentFactory::create();
} catch (\Exception $e) {
// Error Handling
}
if ($payment instanceof \App\Services\CollectOnDelivery) {
// 代引きの場合の処理
} else if ($payment instanceof \App\Services\CreditCard) {
// クレジットカードの場合の処理
} else {
// 領収書払いの場合の処理
}
}
この問題を解決するために、Facadeパターンを利用します。Facadeパターンは、外部に公開するメソッドをシンプルにしておき、具体的な処理は全て隠蔽することで利用者が迷わず使えるようになるデザインパターンです。
具体的にはこんな感じになります。
public function Payment(Request $request)
{
try {
$payment = PaymentFactory::create();
} catch (\Exception $e) {
// Error Handling
}
try {
$payment->execute();
} catch (\Exception $e) {
// Error Handling
}
}
全ての決済クラスにexecuteメソッドを実装し、それを呼びさえすれば決済処理が実行されるようにしておきます。
こうすることで、コントローラー側は支払い方法の種類を一切気にする必要がなくなります。決済処理に変更があっても、それに対応するクラス内の実装を変更するだけで済みますね。
肝心の実装ですが、まず決済クラスが利用するPayWayインターフェースを作成します。
<?php
namespace App\Services;
interface PayWay
{
public function execute();
}
全ての決済クラスは、このPayWayインタフェースを継承するようにします。これにより、利用者は決済クラスごとの差異を気にせず、インタフェース内のメソッド定義をもとにメソッドを呼べるようになります。
あとは、各決済クラスを実装していくだけです。
<?php
namespace App\Services;
use App\Services\Payway;
class CreditCard implement PayWay
{
public function execute()
{
// クレジットカードの場合の決済処理を書いていく
}
}
これで、各支払い方法の具体的な処理を、クラス内に閉じ込めることができました!また、呼び出し側では処理内容を気にせずメソッドを呼ぶことができます。
もちろん、全ての処理を決済クラスで完結させるのではなく、共通部分は別クラスに切り出すことも可能です。結果的に、変更に強いクラス設計になったと思います。
まとめ
「Java言語で学ぶデザインパターン」には23個のパターンが解説されていますが、全てのパターンをそのまま実装に適用するのは無理があると思います。フレームワークがよしなにやってくれてそうな箇所もあったりして、実際にデザインパターンが役に立つケースはそこまで多くないように感じました。
とはいえ、典型的なパターン例を知っておくことで設計、実装の選択肢が広がりますし、トンデモ設計に陥る可能性も減らせると思います。コーディングに慣れてきて、設計に興味が出てきたらぜひ読んでみることをおすすめします。