SOLID原則:Open-Closed Principle
SOLID原則における「 Open-Closed Principle (OCP) 」とは次のことを意味します。
「モジュールは拡張に対して開いて (Open) おり,修正に対して閉じて (Closed) いなければならない」
自分はこの原則をラフに以下のように理解しています。
ソフトウェアの機能追加や仕様変更に対しコード修正ではなくコード追加で対応可能な設計をしようぜ〜
※ラフ過ぎたらすいません...
なぜ今OCPを考えるのか?
自分は新卒でこの業界に入り約5年が経ちますが、機能追加や仕変を実施したことで既存機能にバグが発生する通称デグレに多く遭遇してきました。
デグレにはデグらす人とデグらされる人が存在すると考えます。
①デグらす人
機能追加や仕変に対応し既存機能にバグを発生させた張本人
②デグらされる人
既存機能を実装した人
勿論①②どちらの立場も経験しましたが、基本的にデグレが発生した場合①の人物に責任があると思われます。ただ本当に①のみの責任なのでしょうか?
答えはNoです。
何故なら②の人物が拡張性に乏しくいわゆるイケてない実装を行なっているケースも往々にして存在するからです。
そしてこのイケてない実装を具体的に考えた時にOCP違反しまくっている実装に結びつきました。
※あくまで個人の経験則の話です。
またOCP違反はソフトウェアの安定性だけでなく仕様変更にかかるコストにも直結する問題となることが多いです。
OCP違反の実装
OCP違反している実装をよくある決済処理を例に見てみましょう。(Laravel)
PaymentProcessorServiceはControllerから呼び出されるServiceClassと認識頂ければと思います。PaymentProcessorServiceクラスは以下の仕様を実装してます。
・$gatewayに対応する支払い処理を実施
・決済処理が完了したら支払い完了メールを送信
<?php
namespace App\Http\Services
class PaymentProcessorService
{
public function processPayment($amount, $gateway) {
$paymentResult = false
if ($gateway === 'stripe') {
$paymentResult = $this->processStripePayment($amount);
} elseif ($gateway === 'paypal') {
$paymentResult = $this->processPaypalPayment($amount);
} else if ($gateway === 'credit') {
$paymentResult = $this->processCreditCardPayment($amount);
}
if ($paymentResult) {
$this->sendConfirmationEmail();
}
}
private function processStripePayment($amount) {
// Stripe APIを使用した支払い処理
return true; //ダミーで成功とする
}
private function processPaypalPayment($amount) {
// PayPal APIを使用した支払い処理
return true; //ダミーで成功とする
}
private function processCreditCardPayment($amount) {
// クレジットカード決済の時の支払い処理
return true; //ダミーで成功とする
}
private function sendConfirmationEmail() {
$message = "Payment successful.\n";
// 支払い確認のメールを送信する処理
}
}
では以下の仕様変更が発生しました。
・決済方法に電子マネー決済が追加
・メール内容を決済方法ごとに変更
<?php
namespace App\Http\Services
class PaymentProcessorService
{
public function processPayment($amount, $gateway) {
$paymentResult = false;
if ($gateway === 'stripe') {
$paymentResult = $this->processStripePayment($amount);
} elseif ($gateway === 'paypal') {
$paymentResult = $this->processPaypalPayment($amount);
} elseif ($gateway === 'credit') {
$paymentResult = $this->processCreditCardPayment($amount);
} elseif ($gateway === 'e-wallet') {
$paymentResult = $this->processEwalletPayment($amount);
}
if ($paymentResult) {
$this->sendConfirmationEmail($gateway);
}
}
private function processStripePayment($amount) {
// Stripe APIを使用した支払い処理
return true; // ダミーで成功とする
}
private function processPaypalPayment($amount) {
// PayPal APIを使用した支払い処理
return true; // ダミーで成功とする
}
private function processCreditCardPayment($amount) {
// クレジットカード決済の時の支払い処理
return true; // ダミーで成功とする
}
private function processEwalletPayment($amount) {
// 電子マネー決済の支払い処理
return true; // ダミーで成功とする
}
private function sendConfirmationEmail($gateway) {
// 支払い確認のメールを送信する処理
if ($gateway === 'stripe') {
$message = "Payment Stripe successful.\n";
} elseif ($gateway === 'paypal') {
$message = "Payment Paypal successful.\n";
} elseif ($gateway === 'credit') {
$message = "Payment Credit successful.\n";
} elseif ($gateway === 'e-wallet') {
$message = "Payment E-wallet successful.\n";
}
// メール送信処理
}
}
雑に実装しましたが、恐らく上記のようになると思われます。
勿論もっと良い実装方法はあると思いますが、いずれにせよPaymentProcessorクラスを修正することに変わりはなさそうです。
将来的に更に決済方法が追加になったり、クレジットカード会社固有の仕様が発生すると更にPaymentProcessorクラスに多くの修正が発生し肥大化します。
これではデグレを生む可能性が高くなると思われます。
OCPに準拠した実装
「モジュールは拡張に対して開いて (Open) おり,修正に対して閉じて (Closed) いなければならない」この「開いているのに閉じている」矛盾を実現するための具体的手法は以下だと考えます。
変更の可能性があり、且つ変更の影響が一番大きいホットスポットを抽象化してClassにする。
具体的に見ていきましょう。まず仕様変更前の実装になります。
<?php
namespace App\Http\Services;
interface PaymentProcessorInterface {
public function processPayment($amount);
public function sendConfirmationEmail();
}
<?php
namespace App\Http\Services;
class StripePaymentGateway implements PaymentGateway
{
public function processPayment($amount): bool {
// Stripe APIを使用した支払い処理
return true; // ダミーで成功とする
}
public function sendConfirmationEmail() {
$message = "Payment successful.\n";
// メール送信処理
}
}
<?php
namespace App\Http\Services;
class PaypalPaymentGateway implements PaymentGateway
{
public function processPayment($amount): bool {
// PayPal APIを使用した支払い処理
return true; // ダミーで成功とする
}
public function sendConfirmationEmail() {
$message = "Payment successful.\n";
// メール送信処理
}
}
<?php
namespace App\Http\Services;
class CreditCardPaymentGateway implements PaymentGateway
{
public function processPayment($amount): bool {
// クレジットカード決済の時の支払い処理
return true; // ダミーで成功とする
}
public function sendConfirmationEmail() {
$message = "Payment successful.\n";
// メール送信処理
}
}
<?php
namespace App\Http\Services;
use App\Http\Services\PaymentProcessorInterface;
use App\Http\Services\StripePaymentGateway;
use App\Http\Services\PaypalPaymentGateway;
use App\Http\Services\CreditCardPaymentGateway;
class PaymentProcessorService
{
public function paymentGateway($gateway): PaymentProcessorInterface {
switch ($gateway) {
case 'stripe':
return new StripePaymentGateway();
case 'paypal':
return new PaypalPaymentGateway();
case 'creditcard':
return new CreditCardPaymentGateway();
default:
return null;
}
}
public function processPayment($amount, PaymentProcessorInterface $paymentGateway) {
if ($paymentGateway->processPayment($amount)) {
$paymentGateway->sendConfirmationEmail();
}
}
}
今回のホットスポットは完全に"決済方法"であることはお分かり頂けると思います。
そのため決済方法セグメントでClass化しました。
またPaymentProcessorInterfaceを定義することで、インターフェイス分離の原則にも準拠します。
Controllerは以下のような実装です。
<?php
use Illuminate\Http\Request;
use App\Http\Services\PaymentProcessorService;
class PaymentProcessorController
{
protected $paymentProcessorService;
public function __construct(PaymentProcessorService $paymentProcessorService) {
$this->paymentProcessorService = $paymentProcessorService;
}
public function processPayment(Request $request) {
$requestData = $request->all();
$paymentGateway = $this->paymentProcessorService->payMentGateway($requestData['gateway']);
$this->paymentProcessorService->processPayment($requestData['amount'], $paymentGateway);
}
}
では以下の仕様変更に対応しましょう。
・決済方法に電子マネー決済が追加
<?php
namespace App\Http\Services
class ElectronicPaymentGateway implements PaymentGateway
{
public function processPayment($amount): bool {
// 電子決済の時の支払い処理
return true; // ダミーで成功とする
}
public function sendConfirmationEmail() {
$message = "Payment successful.\n";
// メール送信処理
}
}
<?php
namespace App\Http\Services;
use App\Http\Services\PaymentProcessorInterface;
use App\Http\Services\StripePaymentGateway;
use App\Http\Services\PaypalPaymentGateway;
use App\Http\Services\CreditCardPaymentGateway;
use App\Http\Services\ElectronicPaymentGateway; // ←追加
class PaymentProcessorService
{
public function paymentGateway($gateway): PaymentProcessorInterface {
switch ($gateway) {
case 'stripe':
return new StripePaymentGateway();
case 'paypal':
return new PaypalPaymentGateway();
case 'creditcard':
return new CreditCardPaymentGateway();
case 'e-wallet': // ←追加
return new ElectronicPaymentGateway();
default:
return null;
}
}
public function processPayment($amount, PaymentProcessorInterface $paymentGateway) {
if ($paymentGateway->processPayment($amount)) {
$paymentGateway->sendConfirmationEmail();
}
}
}
決済方法の追加という拡張に対してコードの追加で対応出来ました。
・メール内容を決済方法ごとに変更
こちらは各ClassのsendConfirmationEmail()の$messageの値を変更すれば良いので割愛します。変更の影響は各クラスのsendConfirmationEmail()内に収まるため、全体としては修正に対してClosedしていると考えられます。
これで無事にOCPに準拠した実装が出来たと思います...
と言いたいところですが、以下の部分がまだ完全に修正に対してClosed出来ておりません。
途中でいやコード修正してね?と思った方は鋭いっす。
<?php
namespace App\Http\Services;
use App\Http\Services\PaymentProcessorInterface;
use App\Http\Services\StripePaymentGateway;
use App\Http\Services\PaypalPaymentGateway;
use App\Http\Services\CreditCardPaymentGateway;
use App\Http\Services\ElectronicPaymentGateway;
class PaymentProcessorService
{
public function paymentGateway($gateway): PaymentProcessorInterface {
// 決済方法追加でここのswitchは修正が必要なるため、厳密にはOCP違反
switch ($gateway) {
case 'stripe':
return new StripePaymentGateway();
case 'paypal':
return new PaypalPaymentGateway();
case 'creditcard':
return new CreditCardPaymentGateway();
case 'e-wallet':
return new ElectronicPaymentGateway();
default:
return null;
}
}
~~略~~
}
実際はこれでOKとする場合も往々にしてありますが、折角なので完全にClosed状態に持っていきましょう。
やり方は色々あるっぽいですが、Laravelを使っている前提なので、ServiceProviderでDIしましょう〜
<?php
namespace App\Providers;
use App\Http\Services\PaymentProcessorService;
use App\Http\Services\StripePaymentGateway;
use App\Http\Services\PaypalPaymentGateway;
use App\Http\Services\CreditCardPaymentGateway;
use App\Http\Services\ElectronicPaymentGateway;
class PaymentServiceProvider extends ServiceProvider
{
public function register() {
$this->app->singleton('PaymentProcessorService', function ($app) {
$service = new PaymentProcessorService();
$service->addPaymentGateway('stripe', new StripePaymentGateway());
$service->addPaymentGateway('paypal', new PaypalPaymentGateway());
$service->addPaymentGateway('creditcard', new CreditCardPaymentGateway());
$service->addPaymentGateway('e-wallet', new ElectronicPaymentGateway());
return $service;
});
}
}
そしてPaymentProcessorServiceからswitchを削除して以下に書き換えます。
<?php
namespace App\Http\Services;
class PaymentProcessorService
{
private $paymentGateways = [];
public function addPaymentGateway(string $name, PaymentProcessorInterface $gateway) {
$this->paymentGateways[$name] = $gateway;
}
public function getPaymentGateway(string $name): ?PaymentProcessorInterface {
return $this->paymentGateways[$name] ?? null;
}
public function processPayment($amount, string $gatewayName) {
$paymentGateway = $this->getPaymentGateway($gatewayName);
if ($paymentGateway) {
if ($paymentGateway->processPayment($amount)) {
$paymentGateway->sendConfirmationEmail();
}
} else {
throw new Exception("Invalid payment gateway");
}
}
}
これでOCPに準拠した実装が実現出来たかと思います。
以上です!