[PHP] そのプロパティ、privateに出来ませんか?


前書き

最初に言っておきます、オブジェクト指向をちゃんと理解している人は読む必要のない記事です。おぼろげにしか理解していない人のために、またつい最近までちゃんと理解していなかった自分へのメモのために書きます。

  • プロパティは全て private が当たり前だと思っている人は読まなくていいです。
  • プロパティは全て public が当たり前だと思っている人はもうちょっとクラスの継承・カプセル化について勉強してから読みに来てください。

2014/11/25 タイトル変更
コメント欄の@xipxさんの指摘、ならびにそれに対する私の回答を併せてご覧ください。

問題

外部からのアクセスに対してアクセス修飾子が持つ意味

「プロパティは全部 private が当たり前だ!」とは言いましたが、当然 「継承するときどうするの?」 って思いますよね。ここで例を示します。文字列のみをプロパティとして格納することを許可されたクラスです。

Wordクラス
class Word {

    protected $word = '';

    public function setWord($word) {
        if (!is_string($word)) {
            throw new InvalidArgumentException('invalid argument variable type');
        }
        $this->word = $word;
    }

    public function getWord() {
        return $this->word;
    }

}

こういうコードを書いてる方、多いんじゃないでしょうか。ゲッターとセッターを用意することで外部から

$obj = new Word;
$obj->word = 'hoge';
echo $obj->word;

のようなアクセスは出来ないようにし、

$obj = new Word;
$obj->setWord('hoge');
echo $obj->getWord();

という方法を代わりに提供する手段です。こうすることで、文字列ではないものをセットしようとしたときに例外を発生させることが出来ます。

$obj = new Word;
$obj->word = array();
echo $obj->word; // Notice: Array to string conversion が発生してしまう
$obj = new Word;
$obj->setWord(array()); // この段階で例外がスローされる
echo $obj->getWord();

ところが・・・

継承先のクラスからのアクセスに対してアクセス修飾子が持つ意味

Wordクラス
class Word {

    protected $word = '';

    public function setWord($word) {
        if (!is_string($word)) {
            throw new InvalidArgumentException('invalid argument variable type');
        }
        $this->word = $word;
    }

    public function getWord() {
        return $this->word;
    }

}
MyWordクラス
class MyWord extends Word {

    public function setWord($word) {
        $this->word = $word;
    }

}

このようにノーチェックでセットするようにオーバーライドしたとします。すると、

$obj = new MyWord;
$obj->setWord(array());
echo $obj->getWord(); // Notice: Array to string conversion が発生してしまう

のようにエラーが発生してしまいます。Word::getWord() メソッドは $this->word が文字列であることが前提で書かれたメソッドであり、継承先のクラスでそのルールを破った代入を行ってしまったため、このような結果になってしまったのです。

解決策

こういった問題を防ぐためにどうするかを考えましょう。

プロパティの前にアンダースコアをつける

Wordクラス
class Word {

    protected $_word = '';

    public function setWord($word) {
        if (!is_string($word)) {
            throw new InvalidArgumentException('invalid argument variable type');
        }
        $this->_word = $word;
    }

    public function getWord() {
        return $this->_word;
    }

}

親クラスでアンダースコアを付加して書いておき、 「継承先でアンダースコアから始まるプロパティには勝手に触れないでね!」 というオレオレルールに従わせるという発想です。しかし、これでは

こういう考えの人が

MyWordクラス
class MyWord extends Word {

    public function setWord($word) {
        $this->_word = $word;
    }

}

なんて書いちゃうかもしれませんよね。根本的な解決策にはなってないということです。

プロパティを private にする

実は、継承先で使う場合でも private にすることは可能なんです。

Wordクラス
class Word {

    private $word = '';

    public function setWord($word) {
        if (!is_string($word)) {
            throw new InvalidArgumentException('invalid argument variable type');
        }
        $this->word = $word;
    }

    public function getWord() {
        return $this->word;
    }

}

こうしておけば

MyWordクラス
class MyWord extends Word {

    public function setWord($word) {
        $this->word = $word;
    }

}
$obj = new MyWord;
$obj->setWord(array());

とされても、$obj->getWord() で得られるもの、つまり Word クラスで private $word; と宣言したものには影響を及ぼしません。では実際に何に対する代入が行われたのかというと…

MyWord クラスの public $word; が該当します。

「そんな記述ないじゃないか!」 と突っ込みたくなるかもしれませんが、PHPでは未定義のインスタンスプロパティに対して暗黙的に public で代入することが出来るので、こういった挙動になるわけです。もちろんJava等であればエラー・例外が発生します。

しかし、これでは

$obj = new MyWord;
$obj->setWord('hoge');

という正しい代入を行ったとしても、 $obj->getWord() で得られるものは変化してくれません。そこで、 parentキーワード を使って親クラスのメソッドを呼び出すことにします。

MyWordクラス
class MyWord extends Word {

    public function setWord($word) {
        parent::setWord($word);
    }

}

こうしておけば、文字列であるかどうかのチェックを行った上で Word クラスで private $word; と宣言したもの に代入することが出来ます。プロパティ自体は private ですが、親クラスの setWord メソッドを通じてならばアクセスすることが可能になるからです。

追記

(あくまで private を活用したデザインパターン例として説明したかったのですが)コメント欄で 「継承メソッドを作らずにそのまま使えばいいだけじゃない?」 という指摘を頂きました。こうするメリットを明確にしたいならば

MyWordクラス
class MyWord extends Word {

    public function setDateTime(DateTime $date) {
        parent::setWord($date->format('Y-m-d H:i:s'));
    }

}

なんてものを作ってみてもいいかもしれませんね。この場合はメソッドのオーバーライドを行っていないので

MyWordクラス
class MyWord extends Word {

    public function setDateTime(DateTime $date) {
        $this->setWord($date->format('Y-m-d H:i:s'));
    }

}

とすることも出来ます。

結論

private をうまく活用すれば、アンダースコアで protected を表現する構成にする必要がなくなります。

オブジェクト指向(OOP)を正しく理解して、美しいコーディングを心がけましょう。

OPP↓