PHP

PHPで学ぶオブジェクト指向

More than 1 year has passed since last update.

はじめに

効率的なwebアプリケーションという本で学んだ内容を備忘録として記録していきます。
私は今までオブジェクト指向というものを何度も勉強しようとしてみたものの、Humanクラスがどうのこうのとか、Birdクラスを継承したChickenクラスがどうとか・・・意味がわからなさすぎて何度も挫折してきました・・w

そんな中、最近私はSchooというオンラインで授業が受けれるサービスに加入しました。
そこでこの本の著者である小川雄大さんの「オブジェクト指向入門」という授業を開講していたので、受けてみたところ、目から鱗が落ちるくらいの衝撃でオブジェクト指向がスルスルと頭に入って行きました。

そこでもっと本格的にオブジェクト指向を勉強しようと思って、小川雄大さんの書かれた「効率的なwebアプリケーション」を購入し、ここにまとめようと思った次第であります。

オブジェクト指向の3大要素

  • ポリモーフィズム
  • カプセル化
  • 継承

この3つを理解すると、オブジェクト指向がわかってきます

ポリモーフィズム(多様性)

オブジェクト指向で重要な考え方となるポリモーフィズムとは、同じメソッドの呼び出しであっても、オブジェクトが変われば振る舞いが変わることを言います。

と言ってもよくわからないので例で見てみます。
例えば下記のような2つの関数(perfomelog)があった場合、現在ログの出力方法が標準出力になっていますが、これをファイルにログを残したいという仕様変更があったらどうしますか。
log メソッドを直接書き換える必要が出てきてしまいます。
そうすると、元のプログラムに修正を入れる必要があるため、修正が困難になってしまいます。

function perfome()
{
    log('peform');
}

function log($message)
{   
    echo $message;
}

じゃあ、ここにオブジェクト指向を導入するとどうなるか。

function perfome($logger)
{   
    $logger->log('perform');
}   

class EchoLogger
{   
    public function log($message)
    {
        echo $message;
    }
}   

class FileLogger
{   
    protected $fileWriter;


    public function log($message)
    {
        $this->fileWriter->write($message);
    }
}   

perfome 関数は引数に $logger オブジェクトを受け取り、そのlogメソッドを呼ぶ形に変わっています。
これが先ほどの例と何が違うのか。

先ほどの例では、ログの出力を標準出力からファイル出力に変更したい場合、元のコードの log 関数を直接編集する必要がありました。
ですが、今回の例では perfome 関数は、引数で受け取った$loggerオブジェクトのlogというメソッドを呼び出しているだけです。

つまり、ログの出力方法を変更したい場合、元のコードは修正することなく、perfome関数に渡していたオブジェクトを EchoLogger オブジェクトから FileLogger に変更するだけで良いのです。
これが冒頭に述べた、 同じメソッドの呼び出しであってもオブジェクトが変われば振る舞いが変わる というポリモーフィズム(多様性)なのです。

実装と抽象

私は今までこの「実装」と「抽象」の違いがよくわかりませんでした。
というのも、私が勉強していた時はこの違いの説明すら”抽象的”に感じてしまって、理解に苦しんでいたのです。

では、違いはなにかというと、先ほどのログ出力の例で言えば、ログを出力する(どこにどんな形式でログを吐くかは知らない)logメソッドという漠然としたものを「抽象」、その抽象をプログラムに落とし込んだものを「実装」といいます。

これも実際に例を見てみましょう。ここではユーザの認証を管理するAuthenticationManagerクラスを見てみます。

class AuthenticationManager
{
    protected $userRepository;

    public function __construct($userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function authenticate($userName, $password)
    {
        $user = $this->userRepository->findUser($userName);
        if (!$user) {
            throw new UserNotFoundException();
        }

        if (!$user->validatePassword($password)) {
            throw new InvalidPasswordException();
        }

        return $user;
    }
}

このなかのauthenticateメソッドは、中で2つのメソッドの呼び出しを行ってます。

  • $this->userRepository->findUser($userName)
    • $userNameのユーザがいたら、ユーザオブジェクトを返すであろうメソッド
  • $user->validatePassword($password)
    • $passwordがユーザの設定したものと一致してるかどうかをbooleanで返す

ここで大事な考え方は、AuthenticatorManagerはこの2つのメソッドの中身を知らなくても問題がないということです。
findUserが中でSQLを実行しているかもしれませんし、ユーザ一覧が書かれたファイルから該当行を抜き出してるかもしれませんが、そんなことは知らなくて良いのです。
AuthenticatorManagerは、メソッド名引数戻り値の3つだけ知ってればOKなのです。

ただ、このクラスは「実装に依存している」という状態です。
コンストラクタの引数に$userRepositoryオブジェクトを渡せば動作はしますが、ここには何を渡してもOKな状態になってます。
つまり、$userRepositoryがfindUserメソッドを持ってるかどうかなんて、実行してみないとわからないのです。。

インターフェイス

そこで出てくるのがインターフェイスというPHPが提供する言語機能です。
インターフェイスとはまさに「抽象」を定義する存在のことです。
先ほど、「具体的に何するかわからないけど、ログを吐くlogというメソッド」のことを「抽象」と言いましたが、インターフェイスはこの「抽象」を定義します。

実際に見てみましょう。

interface UserRepositoryInterface
{
    /**
     * @param string $userName
     * @return UserInterface
     */
    function findUser($userName);
}

interface UserInterface
{
    /**
     * @param string $password
     * @return boolean
     */
    function validatePassword($password);
}



class AuthenticationManager
{
    protected $userRepository;

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function authenticate($userName, $password)
    {
        $user = $this->userRepository->findUser($userName);
        if (!$user) {
            throw new UserNotFoundException();
        } elseif (!$user instanceof UserInterface) {
            throw new LogicException('User must implement UserInterface');
        }

        if (!$user->validatePassword($password)) {
            throw new InvalidPasswordException();
        }

        return $user;
    }
}

これでこのAuthenticatorManagerクラスを「抽象に依存」させることが出来ました。
ポイントが2つあります。

  • 1. コンストラクタで$userRepositoryはUserRepositoryInterfaceという型指定を行っている
    • UserRepositoryInterfaceでは、「引数に$userNameという文字列を渡すと、UserInterfaceを実装したクラスを返すfindUserメソッドを定義」しています
    • つまり、先ほどまでの「実装に依存」している状態で心配していた$userRepositoryオブジェクトにfindUserメソッドがあるかどうか、という心配はいらなくなりました。
  • 2. findUserメソッドの戻り値がUserIntefaceを実装したクラスかチェックしてる
    • これによって、$userオブジェクトはUserInterfaceを実装したクラスがかえってくることが保証されました
    • UserIntefaceでは、「引数に$passwordという文字列を受け取るとbooleanを返すvalidatePasswordというメソッドを定義」してます
    • つまり、$userオブジェクトがvalidatePasswordというメソッドを持っていないかもという心配がいらなくなってます

カプセル化

だいぶ久々になってしまいましたが、カプセル化についても書いてみようと思います。
今まで、全然理解出来てなくて、「カプセル化って、要は変数名をprivateにすりゃいいんでしょ?」くらいの認識でした。

カプセル化いうのは、クラスの呼び出し側が、参照しているクラスの状態など知らなくても良いような状態だと思っています。
例えばですが、下のような、ユーザの利用言語だったら英語で出力するような処理があったとします。

class User
{
    public $useLanguage = ''; 

    public function setLanguage($lang)
    {   
        $this->useLanguage = $lang;
    }   
}

class Display
{
    private $user;

    public function __construct(User $user)
    {   
        $this->user = $user;
    }   

    public function showMessage($japaneseMessage, $englishMessage)
    {   
        if ($this->user->useLanguage === 'ja') {
            echo $japaneseMessage.PHP_EOL;
        } elseif ($this->user->useLanguage === 'en') {
            echo $englishMessage.PHP_EOL;
        }   
    }   
}

$user = new User();
$user->setLanguage('ja');

$display = new Display($user);
$display->showMessage('こんにちわ', 'hello');
// こんにちわ

$user->setLanguage('en');

$display = new Display($user);
$display->showMessage('こんにちわ', 'hello');
// hello

ここで問題になってくるのは、UserクラスのuseLnaguage(ユーザの利用言語)がpublicになっていること。
また、それをDisplayクラスが参照してしまっていることです。

では、実際にどのようなときに問題が起きるでしょうか。
一番簡単な例。
ユーザの利用言語を表現するのがjaenではなくて、japaneseenglishになったらどうでしょうか。
もしこの処理が30箇所にかかれていたら・・・
超不毛な修正が必要になってしまいます。。

さらに、追加で 利用言語が英語でも、ユーザの居住地が日本だったら日本語にしてくれという仕様変更があったらどうでしょうか。
showMessageの中をこう修正しますか。

        if ($this->user->useLanguage === 'japanese') {
            echo $japaneseMessage.PHP_EOL;
        } elseif ($this->user->useLanguage === 'english') {
            // 利用言語が英語でも所在地が日本なら日本語を出す
            if ($this->user->address === 'japan'){
                echo $japaneseMessage.PHP_EOL;
            } else {
                echo $englishMessage.PHP_EOL;
            }
        }

辛いですよね。。。

つまり、日本語/英語のどちらを表示するのかという判断を、Userクラスの状態を見て他のクラスが判定しているからこんなに大変になってしまう訳です。

そこで、Userクラスは完全に言語や所在地の情報を隠蔽し、日本語で表示すべきかというメソッドだけを持っていたらどうでしょう。

class User
{
    private $useLanguage = '';
    private $address     = '';

    public function __construct($lang, $address)
    {
        $this->useLanguage = $lang;
        $this->address     = $address;
    }

    public function isDisplayJapaneseMessage()
    {
        if ($this->useLanguage === 'ja' || $this->address === 'japan'){
            return true;
        }

        return false;
    }

}

class Display
{
    private $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function showMessage($japaneseMessage, $englishMessage)
    {
        if ($this->user->isDisplayJapaneseMessage()) {
            echo $japaneseMessage.PHP_EOL;
        } else {
            echo $englishMessage.PHP_EOL;
        }
    }
}

$user = new User('ja', 'japan');

$display = new Display($user);
$display->showMessage('こんにちわ', 'hello');

$user = new User('en', 'japan');
$display = new Display($user);
$display->showMessage('こんにちわ', 'hello');

これで、何語で表示すべきかという情報はUserクラスに隠蔽することができました。
これを呼び出し、判別するという処理を書いている部分を修正することなく、呼び出し側はisDisplayJapaneseMessageというメソッドがtrueを返した時だけ日本語で表示すれば良い。
これだけを気にすればよく、Userクラスの状態に依存することがなくなりました。
このようにクラスの持つ状態をカプセル化といいます。

(この理解が自分でもまだ正しいのか不安です・・w)

※ まだ編集途中なのでこれから追記していきます - 2015.06.08

カプセル化についての追記

カプセル化の本質は、複雑な部分を隠すという事です。
私は以前の投稿で、カプセル化について以下のような理解をしてます。

カプセル化いうのは、クラスの呼び出し側が、
参照しているクラスの状態など知らなくても良いような状態だと思っています。

参照元(使う人)が参照先(利用物)の 状態 を知らなくて良い。
というのは確かにカプセル化の1つですが、かなり限定的な理解に思えます。

今の私の理解では、カプセル化は複雑な部分を隠し、使う人はその複雑な中身を知らなくても良い。
という感じです。

どういう事かというと、例えば日常生活にもこういった事はあふれています。
例えば車。私がアクセルを踏むと車は進みます。
当たり前のように思えますが、実際は内部的にはアクセルを踏むことでワイヤーが引かれ、
そのワイヤーの繋がる先のバルブの開閉度が変化することで、吸気量が変化し、エンジンの爆発エネルギーが増して、回転数が上がる事で、タイヤの回転数も上がり、車体を加速させ。。のような事が行われてます。

の、様に実際には利用物である参照先(車)が内部でやってる複雑な事を隠蔽して、
アクセルを踏む という行為だけを参照元(運転手)に公開して、運転手は「アクセルを踏めば加速する」という事だけ知ってれば良い。
という状態にすることがカプセル化だと思ってます。