10週間ウェブ開発講座

裏メニュー第四週

アクセス修飾子と抽象クラス、インタフェース

先週分の講座で拾いきれなかったPHPのクラス定義に関する諸トピックについて説明します。

アクセス修飾子

メンバ変数やメンバ関数には適切なアクセス修飾子を指定するべきです。
アクセス修飾子とは、メンバ変数やメンバ関数が参照され得る範囲を指定するためのキーワードです。
アクセス修飾子には次のようなものがあります。

  • public (どこからでもアクセス可能*1)
  • protected (派生クラスや親クラスからアクセス可能)
  • private (自身を定義するクラスからのみアクセス可能)

不必要にアクセス可能な範囲を広げることは、バグを埋め込むことに繋がり易いです。
メンバ変数やメンバ関数には必要最小限のアクセス範囲を指定するように心掛けましょう。

<?php
class Sample {
  public  $public_var;
  private $private_var;

  public function __construct() {
    $this->public_var = "Hi! I'm a public variable.";
    $this->private_var = "Hi! I'm a private variable.";
  }
}

$sample = new Sample();
echo "{$sample->public_var}\n"; //=> "Hi! I'm a public variable.";
echo "{$sample->private_var}\n"; //=> "Fatal error: Cannot access private property Sample::$private_var"
?>

14行目で「Fatal error」すなわち「致命的なエラー」が発生します。
これはprivateな変数を外部から参照しようとしたことが原因です。

「控え目である」もしくはカプセル化

参考資料として挙げている「PHP でオブジェクト指向の設計をするための 7 つの良い習慣を身につける」に次のような記述があります。

控え目であるということは、クラスや関数の実装の中で中身が見えないようにすることです。
情報を隠すことは基本的な習慣です。実装の詳細を隠す習慣を身につけない限り、
他のどのような習慣を身につけるのも難しいものです。
情報を隠すことはカプセル化としても知られています。

public フィールドを直接公開することがなぜ不適切なのか、
その理由は数多くありますが、なかでも最も重要な理由は、
実装の中で何かが変更された場合にいかなる方法でも対処しきれなくなることです。
OO の概念を使うと変更を分離することができ、
またいかなる変更の影響も実質的に他には伝染しないことを確実にする上で、
カプセル化が重要な役割を果たします。
伝染性のある変更というのは、最初は些細な変更から始まるものです
(例えば 3 つの要素を含む配列を変更して 2 つの要素しか含まない配列にする、など)。
ところが突然、些細な変更であったはずのものが、
その 1 つの変更に対応するために次々にコードを変更する羽目になっていることに気付くのです。

情報を隠すための手始めとして簡単な方法は、
フィールドを private にし、public アクセサーを使ってフィールドを公開する方法です
(public アクセサーは家の窓のようなものです)。
つまり壁全体を外に向けて開け広げるのではなく、1 つか 2 つの窓のみを持つようにするのです。

引用した文章をPHPプログラムに落し込むと次のようになります。

  1. <?php
  2. class Sample {
  3.   private $private_var;
  4.  
  5.   public function __construct() {
  6.     $this->private_var = "Hi! I'm a private variable.";
  7.   }
  8.  
  9.   public function getPrivateVar() {
  10.     return $this->private_var;
  11.   }
  12. }
  13.  
  14. $sample = new Sample();
  15. // echo $sample->private_var; // 実行するとエラー
  16. echo $sample->getPrivateVar(); // => "Hi! I'm a private variable."
  17. ?>

getPrivateVar?()でオブジェクトのプライベートな変数の値が返ってくることが確認できましたね。

「家族の一員として扱う」もしくは抽象クラス

再度「PHP でオブジェクト指向の設計をするための 7 つの良い習慣を身につける」から引用します。

私が技術リーダーや技術アーキテクトをしているソフトウェア・チームの人達に対して、
私はよく、OO 言語の最大の敵はコピー・アンド・ペースト操作だと言い聞かせています。
最初から OO 設計がされていない場合に、
あるファイルから別のファイルへコードをコピーすることほど大きな混乱を招くものはありません。
あるクラスから別のクラスへコードをコピーしようと思った時には必ず、いったん立ち止まり、
クラスの階層構造を使うことによって類似の機能や同じ機能を活用できないかを考えてみることです。
設計が適切であれば、ほとんどの場合はコードをコピーする必要がまったくないことに気付くはずです。

先週分の講座でお話したように、クラスを継承することで親クラスと同等の能力を持つ子クラスを作成することができます。
継承の機能を使うことで、コピー・アンド・ペーストが必要になるケースが著しく少なくなることを理解しましょう。

継承されるためだけ(親クラスになるためだけ)に定義されるクラスを抽象クラスと呼びます*2
抽象クラスはクラス定義時にabstractキーワードを指定することで作成します。
abstractキーワードのついたクラスはインスタンス化*3できません。

  1. <?php
  2. abstract Class Person {
  3. }
  4.  
  5. class Employee extends Person {
  6. }
  7.  
  8. $person = new Person(); // "Fatal error: Cannot instantiate abstract class Person"
  9. $employee = new Employee();
  10. ?>

8行目でPersonクラスのオブジェクトを作ろうとして「Fatal error」が発生します。 これは抽象クラスとして定義したクラスをインスタンス化しようとしたことが原因です。

「メドゥーサを見ないようにする」もしくはインタフェース

またしても「PHP でオブジェクト指向の設計をするための 7 つの良い習慣を身につける」からの引用です。

私は OO の概念を初めて学んだとき、
インターフェースが本当に役立つものか疑問に思っていました。
すると私の同僚が比喩として、
インターフェースを使わないのはメドゥーサの頭を見るのと同じだという話を紹介してくれました。
メドゥーサは、ギリシャ神話に登場する、髪の毛が蛇にされた女性です。
メドゥーサを直接見た人は誰でも石にされてしまいます。
ペルセウスはメドゥーサを殺しますが、
彼は自分の盾に映る彼女の姿を見ることで彼女に立ち向かうことができ、石にされずにすんだのです。

インターフェースはメドゥーサに立ち向かうための鏡すなわち盾に当たります。
専用の具象実装を使ってしまうと、その実装コードが変更された場合に、
それに合わせて皆さんの作成したコードも変更しなければなりません。
実装を直接使うということは、実質的にクラスを石に変えてしまうことになるため、
多くの選択肢が制限されてしまいます。

インタフェースを使用する例を次に示します(単体では動作しません)。

  1. <?php
  2. interface PersonProvider
  3. {
  4.   public function getPerson($givenName, $familyName);
  5. }
  6.  
  7. class DBPersonProvider implements PersonProvider
  8. {
  9.   public function getPerson($givenName, $familyName)
  10.   {
  11.     // データベースからデータを取得するつもり
  12.     $person = new Person();
  13.     $person->setSomething(xxx);
  14.     return $person;
  15.   }
  16. }
  17.  
  18. class FilePersonProvider implements PersonProvider
  19. {
  20.   public function getPerson($givenName, $familyName)
  21.   {
  22.     // ファイルからデータを取得するつもり
  23.     $person = new Person();
  24.     $person->setSomething(xxx);
  25.     return $person;
  26.   }
  27. }
  28.  
  29. class PersonProviderFactory
  30. {
  31.   public static function createProvider($type)
  32.   {
  33.     if ('database' === $type) {
  34.         return new DBPersonProvider();
  35.     } elseif ('file' === $type) {
  36.         return new FilePersonProvider();
  37.     } else {
  38.         return new NullProvider();
  39.     }
  40.   }
  41. }
  42.  
  43. // Personクラスのオブジェクトが欲しい
  44. $provider = PersonProviderFactory::createProvider('database');
  45. $person = $provider->getPerson('太郎', 'MySQL');
  46.  
  47. // do something
  48. ?>

PersonProvider?インタフェースによりgetPerson()がオーバーライドされることが約束されます。
例えば「データベースでもファイルでもない場所からデータを探したい」とき、
このプログラムにどのような修正を加えればよいでしょうか。 PersonProvider?インタフェースを実装した新しいクラスを作成し、
PersonPrivoderFactory?からそれをインスタンス化したものを返すための分岐を作れば良いですね。
注目すべき点はPersonクラスに対する変更が一切発生しない点です。

宿題

  • publicでない変数が存在する理由について説明してください
  • 抽象クラスが存在する理由について説明してください
  • インタフェースが存在する理由について説明してください
  • (オプション:やや難) 「具象クラスは継承すべきでない」と主張するプログラマが存在します
    • その意見について賛成か反対かの立場を表明してください
    • 賛成もしくは反対の立場をとる理由を説明してください

参考資料


*1 publicは先週の講座で説明したvarと同様の意味です。これはPHP4がpublicな範囲しか持てなかったことの名残りです。
*2 比較のため、抽象クラスでないクラスを具象クラスと呼ぶこともあります。
*3 クラス定義を元にオブジェクトを作成すること。平たく言うとnewすること。

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2010-04-20 (火) 09:12:24 (138d)