はじめに
前回は、webアプリケーションはコードが混同しやすく、MVCという考え方が導入されたという話に触れました。
今回は、オブジェクト指向の基本的な考え方を学びつつ、何が良くなるのか
という点に絞っていきます。
参考図書: 効率的なWEBアプリケーションの作り方
ポリモーフィズム
前回も学んだ様に、オブジェクト指向の原則にOCP(拡張閉鎖原則)というものがあり、
拡張に対しては開いて、修正に対しては閉じるべきという原則があります。
ポリモーフィズムはまさにその原則そのもののようなものです。
つまり、既存のコードを修正することなく、クラスを追加する事で振るまいを変えれる。という状態を実現するものです。
例を見てみましょう。
ポリモーフィズムではない例
最近はものすごい勢いで携帯が進化してますので、携帯で相手に連絡する
という振る舞いを考えてみましょう。
function communicate($friend)
{
// $friendに電話をかける
}
はい。communicateに$friendという友達オブジェクトを渡すと良い感じに電話してくれます。
でも携帯は進化して、スマホになりました。
スマホでは、電話よりもLINEで連絡することが多くなってきてますので、ガラケーだったら電話。スマホだったらLINEで連絡するようにしてみましょう。
function communicate($friend)
{
if (ガラケー) {
// $friendに電話をかける
} elseif (スマホ) {
// $friendにLINEする
}
}
if文を追加して、既存のコードを修正しました。
この様に、OCPに反し、ポリモーフィズムを使わない場合、
振る舞いを変えたくなった時にどんどん既存コードを改修してく必要があります。
じゃあポリモーフィズムだとどうなるのか。
function communicate($phone, $friend)
{
// $phoneのcommunicateを使って、$friendに連絡する
$phone->communicate($friend);
}
class Garke
{
public function communicate($friend)
{
// $friendに電話する
}
}
$garake = new Garke();
// ガラケーで電話して友達にコミュニケーションする
communicate($garake, $friend);
このように、既存コード(communicateメソッド
)は、受け取った$phoneオブジェクトのcommunicateで連絡が取れるようにしておくことで、
振る舞いを変える為にスマホを追加した場合でも、スマホ用のクラスを作り、使う人はスマホクラスのインスタンスを渡して上げれば、「既存コードを改修することなく、振る舞いを変える事」ができます。
function communicate($phone, $friend)
{
// phoneのcommunicateを使って、$friendに連絡する
$phone->communicate($friend);
}
class Garke
{
public function communicate($friend)
{
// $friendに電話する
}
}
class Sumaho
{
public function communicate($friend)
{
// $friendにLINEする
}
}
$garake = new Garke();
$sumaho = new Sumaho();
ommunicate($garake, $friend); // 電話する
communicate($sumaho, $friend); // LINEする
ポリモーフィズムのまとめ
こうすると何が良いかといえば、既存のコードを改修しなくて良いので、
これまで動いていたものは何も気にする必要はなく、新しく作ったスマホクラスだけテストで動作の保証ができてればOKとなります。
そうすると、あるあるな「なんか怖くて振る舞いを追加したくない」という状況を避ける事が出来ます。
振る舞いを変えたり、追加するには新しいクラスを量産するだけでOKなので。
このポリモーフィズムの考え方はデザインパターンにも取り入れられていて、一番分かりやすいのはストラテジーパターンです。
その辺も今度投稿します。
実装と抽象
ポリモーフィズムで分かった様に、オブジェクト指向を使っていく事で、メソッド呼び出しの手順は変更することなく、振る舞い(実装の処理内容)を影響範囲を少なくした状態で変更することが可能になります。
先ほどのcommunicate
メソッドの例で言えば、引数に受け取った$phoneのcommunecateを呼び出せば、$friendに連絡を取ることが出来る。
というメソッドの呼び出し方を知っていますが、実際にどのように連絡をとっているか(実装)は知らなくてもOKでした。
オブジェクト指向の原則には、「抽象に依存せよ」というものもあるように、
メソッドの具体的な実装は知らずに、抽象(メソッドの呼び出し方)だけを知りながら設計をしていくことで、それが自然とOCPに則したコードになっていきます。
抽象とインターフェイス
さて、先ほどのcommunicateメソッドの例では、$phoneがcommunicateメソッドを実装したオブジェクトかどうか、どうやって判別すればよいのでしょうか?
正直、判別方法はありません。
communicate
が未実装であれば「undefined method」で怒られますし、
仮に実装されてても、期待外の振る舞いであればそれが実行されてしまいます。
また、今回のように1メソッドだけなら良いですが、
$phoneに実装されてるはずの5つのメソッドを使うような処理になってた場合、大変ですよね。
しかも、不安ですよね。
そのような問題を解決してくれるのがインターフェイスという仕組みです。
インターフェイスは、先ほどの「抽象」を定義するための存在です。
どういう事かというと、「抽象」はメソッドの呼び出し方の事だったように、
これを定義するものです。
実際に見る方が早いです。
interface CommunicationInterface
{
/**
* $fiendに連絡を取る
*
* @param $friend Friend
*/
public function communicate($friend);
}
このように、class
ではなく、interface
で定義します。
見て分かるように、クラスみたいに「実装」ではなく、メソッドの呼び出し方の抽象だけを宣言してます。
このインターフェイスを使う場合は、implements
演算子を使います。
試しにGarakeクラスを変えてみましょう。
class Garke implements CommunicationInterface
{
}
$garake = new Garake();
// < PHP Fatal error: Class Garke contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (CommunicationInterface::communicate)
このように、implementsしたインターフェイスのメソッドを実装してない場合は、Fatalエラーとして教えてくれます。
タイプヒンティング
さて、このインターフェイスというのは何を解決するためのものだったか。
あるクラスが、特定のメソッドを持ってることを約束して、「あれ?メソッドがないぞ?」とか「なんか期待外の動きするぞ?」という問題をなくすものでした。
それを実現する仕組みとして、タイプヒンティングというものがあります。
例えば、最初のcommunicate
メソッドをタイプヒンティングを使ってみます。
function communicate(CommunicationInterface $phone, $friend)
{
// $phoneのcommunicateを使って、$friendに連絡する
$phone->communicate($friend);
}
$garake = new Garake();
communicate($garake, $friend);
communicateの第一引数$phone
の前にCommunicationInterface
という型の宣言をしました。
PHPでは型を気にする機会が少ないので、違和感を感じるかもしれませんが、
これは「引数はこの型じゃないとダメですよ」っていうお約束です。
これをタイプヒンティングと呼びます。
これまでの実装では、正直$phoneオブジェクトはどんなクラスのものでもOKでした。
しかし、タイプヒンティングを使うことで、「CommunicationInterfaceを実装したクラス」を受け取るというお約束が出来たので、自分が意図したcommunicate
メソッドを必ず呼び出せるようになってます。
事故がなくなりそうですね。
カプセル化
さぁ、オブジェクト指向的な考え方がだんだんとわかってきて面白くなってきました。
オブジェクト指向では、オブジェクトを差し替える事で振る舞いを変えれるという強みを持っていますが、これがいきなりやろうと思ってもなかなかうまくいきません。
じゃあ、どうすればうまくいくのでしょう。
ポイントは、メソッドの呼び出し側に「抽象だけ」を意識させることです。
抽象だけを意識することで、呼び出し側はメソッドの振る舞いが具体的にどのように処理されてるのかを知る必要はありませんでした。
「抽象」という言葉がわかりづらいかもしれませんが、「抽象」を利用者するユーザ目線。「実装」をプログラm目線と考えると分かりやすいかもしれません。
抽象と実装の考え方の違いの例
「Cartクラスを作る際に、在庫があればカートに商品を追加する。という振る舞いを考えてみましょう」
- 抽象的な考え方 - 呼び出し側であるCartクラスを考えます。こちらは呼び出すメソッドの振る舞いだけ知ってれば良いです
- Amazonで「カート」に追加ボタンをおした時の事を考えてみましょう
- ユーザは、ボタンを押すだけでおしまいです。
- 在庫があれば、カートに商品が追加されるし、なければ追加出来なかった旨が表示されるでしょう
- 実装的な考え方 - 実装は呼び出される側の、在庫があるかを実装するProductクラスを考えます
- 商品の在庫があるかどうかを調べる
- 在庫があればtrueを返し、なければfalseを返すメソッドを実装する
このように、呼び出す方はユーザ視点にたって抽象だけを意識して設計するとうまくいきます。
下記が実際に参考図書のリスト2.8
を写経したものです。
CartのisAvailableメソッドは、引数に渡された$productがisAvailable
というメソッドを持っている事、それがbooleanを返すことだけを知っている状態です。
つまり、$productが実際にどうやって在庫の判別をしているか(実装)を知らなくて済んでますよね。
これがオブジェクトの状態(在庫状態)のカプセル化です。
ゆえに、$productにProductクラスのオブジェクトを渡せば在庫状況を見てカートへの追加が出来るし、
$productにDigitalContentsクラスのオブジェクトを渡せば、常にカートへの追加ができる。
この動き、どこかで見ませんでしたか?
そう、オブジェクトを差し替えるだけで、振る舞いを変える事が出来るポリモーフィズムです。
このように、抽象を意識して実装していると、自然とオブジェクト指向で書くことが出来るため、
大原則であったOCPに則ったコードになっていくくのです。
class Cart
{
private $products = [];
/**
* 商品に在庫があれば、カートに追加する
*/
public function add($product)
{
if (!$product->isAvailble()) {
throw new ProductNotAvailableException();
}
$this->products->attatch($product);
}
private function attatch($product)
{
$this->products[] = $product;
}
}
class Product
{
protected $isInStock;
public function isAvailble()
{
return $this->isInStock;
}
}
class DigitalContent extends Product
{
public function isAvailble()
{
return true;
}
}
では、抽象もカプセル化も意識しない場合を考えてみましょう。
class Cart
{
private $products = [];
public function add($product)
{
// DigitalContentじゃないオブジェクトが渡ってきて、
// しかもisInStockがfalseだったら在庫がない
if (!($product instanceof DigitalContent) && !$product->isInStock()) {
throw new ProductNotAvailableException();
}
$this->products->attatch($product);
}
}
いかがでしょう。
何がまずいか分かりますか?
このCartクラスは$productの実装を知ってしまってます。
$productには、DigistalContent
とそれ以外のクラスのオブジェクトが渡ってくる可能性を知っていて、
DigitalContent
のオブジェクトの場合は必ずtrueを返すという実装も知っています。
これでは、実装の変更に影響を受けます。
- DigitalContantクラスの名前が変わった
- DigitalContentクラス以外にも、似たようなクラスを渡したくなった
- DigitalContantの実装が変わった
- etc..
等、呼び出してる方の実装が変わる事により、呼び出し側のCartオブジェクトがもろに影響を受けてしまいます。
これはOCPに反してますね。
拡張も、修正も、どちらにしてもCartクラスを修正する必要が出てきてしまいます。
はじめは難しいかもしれませんが、この「抽象だけ」意識して考える癖がついてくるとオブジェクト指向がみるみる染み付いていきます。
継承と委譲
継承は知っているし、使ってこともある。という方が多いのではないでしょうか。
しかし、継承は継承元クラスと非常に強い依存関係にある点と、多重継承が出来ないという点において、誤った使い方をされる事が多くあります。
誤った継承の使い方
class Advertiser
{
protected $price;
protected $name;
protected $image;
public function getPrice()
{
// 広告の掲載単価を返す
}
public function getName()
{
// 広告の名前を返す
}
public function getImage()
{
// 広告の画像を返す
}
}
/**
* 自社広告を扱うクラス
*/
class InternalAdvertiser extends Advertiser
{
public function getPrice()
{
throw new BadMethodCallException('自社広では単価という概念はない');
}
public function getName()
{
// 広告の名前を返す
}
public function getImage()
{
// 広告の画像を返す
}
}
継承は、基本的に親クラスの振る舞いを継承する必要があります。
この誤った利用例では、親クラスの単価を返す
という振る舞いが失われてしまっています。
is-aの関係
継承では、親の振る舞いが失われない様に、「子 is a 親」(子クラスは親クラスである)
という関係になっている必要があります。
つまり、親クラスで出来る事は小クラスで出来る必要があるわけです。
今回の様に、親ではgetPriceできるけど、小では出来ない。という状況だと、
同じ継承をしているのに、一部ではメソッド呼び出しが失敗していたり、
if文で複数の条件分岐が記載されていて、実際の振る舞いが分かりづらくなったりします。
継承は便利ですが、使うときは「子 is a 親」になっているかどうかを確認する必要が有ります。
多重継承の罠
もう1つ気をつけないといけないのは、PHPを含む多くの言語では多重継承が許されてないという点です。
実際に多重継承の点で困るケースというのは、すでに1つ継承しているのに他のクラスを継承したくなった時です。
誤った継承の例
この例では、DBのの接続を扱うDatabaseConnectionとAuthencateMagerという2つのクラスがありますが、どちらも「ログを吐く」という振る舞いを持ちたいが為に、Loggable
を継承しています。
そうするとどうなるか。
例えば両方のクラスで、「異常時にメール・Slackで知らせる」という共通の機能が生まれてしまったらどうするか。
PHPでは多重継承は許されてないので、もう継承は使えません。
(このような場合、両方に実装するか、委譲を使うか等します)
<?php
abstract class Loggable
{
protected static $file;
public static function setLogFile($file)
{
$this->file = $file;
}
public function log($message)
{
$fp = fopen($this->file, 'a+');
fwrite($fp, $message."\n");
fclose($fp);
}
}
class DatabaseConnection extends Loggable
{
public function executeQuery($sql)
{
$this->log(sprintf('Excute query: %s', $sql));
// sql実行処理..
}
}
class AuthenticateManager extends Loggable
{
public function authenticate($user, $password)
{
$this->log(sprintf('Authenticate user: %s', $user));
// 認証処理
}
}
委譲を使ってみる
上の例を委譲を使って書きなおして見ましょう。
LoggerInterfaceというインターフェイスを定義し、それをimplementsしたFileLoggerを作ってみました。
今まで継承を使ってた2つのクラスには、LoggerInterfaceを実装した$loggerというオブジェクトが渡される事になったので、抽象に依存することが出来ましたし、「異常時にメール・Slackで通知する」という処理が出てきても、そのクラスも委譲させて上げることで、クラス間の依存関係を緩めることが出来ます。
interface LoggerInterface
{
public function log($message);
}
class FileLogger implements LoggerInterface
{
private $file;
public function __construct($file)
{
$this->file = $file;
}
public function log($message)
{
$fp = fopen($this->file, 'a+');
fwrite($fp, $message."\n");
fclose($fp);
}
}
class DatabaseConnection
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function executeQuery($sql)
{
$this->logger->log(sprintf('Excute query: %s', $sql));
// sql実行処理..
}
}
class AuthenticateManager
{
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function authenticate($user, $password)
{
$this->logger->log(sprintf('Authenticate user: %s', $user));
// 認証処理
}
}
委譲は非常に便利な考え方で、上の例では、例えばログをファイルじゃなくてDBに残したい。
となった時も、LoggerInterfaceを実装したDabaseLoggerを作り、
呼び出し元ではDatabaseLoggerのオブジェクトを渡せば、実装を変えることなく振る舞いを変える事が出来ます。
これはStrategyパターンというデザインパターンで、委譲はこれらにも応用出来る非常に便利なテクニックです。.