はじめに
以前、書いた記事の続きです。
オブジェクト指向の実践において大事な考え方であるポリモーフィズムについての記事になります。
※今回も記事内の言語はPHPで書いています。
この記事の目的
オブジェクト指向って結局何なの?ということで、「Clean Architecture(Robert C. Martin)」を読んでみるとどうやら「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」であるらしく、継承やカプセル化に比べて、ポリモーフィズムは特に大事な概念のようです。
今回は、このポリモーフィズムってそもそも何なの?ということを記事にしてみようと思います。
ポリモーフィズムって何?
ポリモーフィズム(多態性)とは、「異なるクラスでも、同じメソッドを使って異なる処理を実行できる仕組み」 ことを意味します。ポリモーフィズムを使うと、コードの柔軟性や再利用性が向上し、クラスやメソッドが特定の実装に縛られることなく、異なる型のオブジェクトを扱うことができるのですが、どういうことか具体的な例を見てみましょう。
例えば、ECサイトを使った例を考えてみます。
ECサイトでは、一般的に支払い処理があります。支払い処理の方法として、クレジットカード、PayPal、銀行振込など、さまざまな方法を提供するはずですね。
各支払い方法により処理内容が異なる場合、それぞれに対して具体的な実装が必要になります。
ポリモーフィズムのことは一旦置いといて、各支払い機能をクラスごとに分けて実装し、
注文のクラスからそれぞれの支払いを呼び出してみます。
下記のような関係になります。
上記の画像では、注文クラスが各支払い方法クラスのメソッドに直接アクセスしています。注文クラスが、具体的な各クラスの実装に 依存 してしまっている状態になります。制御の流れ上こうなることが自然ではありますが、親クラスは子クラスの具体的な実装に強く結びついているため、子クラスの変更が親クラスに影響を与える可能性があります。
その結果、次のような問題が生じることがあります。
-
拡張性の低下
新しい支払い方法を追加するたびに、注文クラスに新たな支払い方法の処理を追加する必要があります。例えば、銀行振込に加えて「Bitcoin」などの支払い方法を追加する場合、注文クラスに直接「Bitcoin」用の処理を追加しなければならず、コードの変更範囲が広がってしまいます。 -
保守性の問題
1つの支払い方法に変更があった場合、注文クラス自体の修正が必要になります。支払い処理の変更頻度が高い場合、バグの発生リスクが増大します。
など
そこで、ポリモーフィズムを用いて「依存関係の逆転」を行うことでこの問題を解決してみます。
ポリモーフィズムを行うためには、インターフェイス経由でメソッドを用いることで、実践できます。
インターフェイスを用いた場合、以下のような関係になります。
ECサイトの支払い機能の関係図(インターフェイス経由で支払い方法呼び出し)
注文クラスは、各支払方法のクラスを直接参照してません。インターフェイスを用いることによって制御の流れを逆転させることができます。
このように、ポリモーフィズムとは、「システムにあるモジュール同士の依存関係を絶対的に制御するための方法」であると言えます。
ポリモーフィズムを実際に書いてみる
ポリモーフィズムを利用して、注文クラスが具体的な支払い方法に依存しない設計になるようにしてみます。
こうすることで、注文クラスに新しい支払い方法を追加する際には、その支払いクラスを作成するだけで済み、注文クラス自体に変更を加える必要がなくなります。
その様子をコードで見てみます。
まず、支払い方法のインターフェースを定義します。このインターフェースでは、processPayment という共通のメソッドを宣言します。
支払方法のインターフェイス
<?php
namespace Payment;
interface PaymentMethod {
public function processPayment($amount);
}
次に、各支払い方法(クレジットカード、PayPal、銀行振込)ごとに、PaymentMethod インターフェースを実装するクラスを定義します。
クレジットカード支払いクラス
<?php
namespace Payment;
class CreditCardPayment implements PaymentMethod {
public function processPayment($amount) {
echo "クレジットカードで $amount 円を支払いました。\n";
}
}
PayPal支払いクラス
<?php
namespace Payment;
class PayPalPayment implements PaymentMethod {
public function processPayment($amount) {
echo "PayPalで $amount 円を支払いました。\n";
}
}
銀行振込支払いクラス
<?php
namespace Payment;
class BankTransferPayment implements PaymentMethod {
public function processPayment($amount) {
echo "銀行振込で $amount 円を支払いました。\n";
}
}
続いて、支払方法のメソッドを利用する注文クラスの設計を行います。
注文クラスは、具体的な支払い方法に依存せず、PaymentMethod インターフェースを通じて支払い処理を行うようにします。
注文クラス
<?php
namespace Order;
use Payment\PaymentMethod;
class Order {
private $paymentMethod;
public function __construct(PaymentMethod $paymentMethod) {
$this->paymentMethod = $paymentMethod;
}
public function processOrder($amount) {
$this->paymentMethod->processPayment($amount);
}
}
最後に、注文クラスを使って支払い処理を試しに行ってみます。
実行例
<?php
require_once 'Payment/CreditCardPayment.php';
require_once 'Payment/PayPalPayment.php';
require_once 'Payment/BankTransferPayment.php';
require_once 'Order/Order.php';
use Payment\CreditCardPayment;
use Payment\PayPalPayment;
use Payment\BankTransferPayment;
use Order\Order;
// 支払い方法を切り替える処理
function getPaymentMethod($method) {
switch ($method) {
case 'credit_card':
return new CreditCardPayment();
case 'paypal':
return new PayPalPayment();
case 'bank_transfer':
return new BankTransferPayment();
default:
throw new Exception('無効な支払い方法');
}
}
// クレジットカードで支払い
$paymentMethod = getPaymentMethod('credit_card');
$order = new Order($paymentMethod);
$order->processOrder(10000);
// PayPalで支払い
$paymentMethod = getPaymentMethod('paypal');
$order = new Order($paymentMethod);
$order->processOrder(5000);
// 銀行振込で支払い
$paymentMethod = getPaymentMethod('bank_transfer');
$order = new Order($paymentMethod);
$order->processOrder(15000);
実行結果
クレジットカードで 10000 円を支払いました。
PayPalで 5000 円を支払いました。
銀行振込で 15000 円を支払いました。
このように行うことで、各支払い方法が追加されたり変更されたりしても、Orderクラス自体には変更が必要なくなりました。
以上までが、ポリモーフィズムについての概要と簡単な使い方でした。
まとめ
ポリモーフィズムを利用すると、子クラスの具体的な実装に依存せず、抽象的なインターフェースに依存する設計へ変わります。なぜそんなことをするかというと、コンポーネントごとに独立させた方が、良い事がたくさんあるからです。
今後、またどこかのタイミングでオブジェクト指向に関係する内容を記事にしてみようと思います。
参照
「Clean Architecture 達人に学ぶソフトウェアの構造と設計(Robert C. Martin)」