PHP V5.3 で遅延静的バインディングを使ったオブジェクト指向プログラミングを活用する

オブジェクト指向プログラミングとデザイン・パターンを新たな目で見る

PHP V5.3 では、オブジェクト指向プログラミング (OOP: Object-Oriented Programming) に関するいくつもの問題が遅延静的バインディング (LSB: Late Static Binding) 機能によって解決されています。PHP で OOP をコーディングする場合の問題が LSB によってどう解決されているのか、また LSB が必要となる、よく知られているオブジェクト指向のデザイン・パターンを実装する方法について学びましょう。

Don Denoncourt, Author, Consultant

Don DenoncourtDon Denoncourt は Java 技術、Groovy、Grails、PHP を専門とするフリーのコンサルタント、トレーナー、メンターであり、著作者でもあります。



2011年 2月 15日

オブジェクト指向プログラミング (OOP: Object-Oriented Programming) を十分に理解していれば、データの抽象化、カプセル化、モジュール化、ポリモーフィズム、そして継承を使用することで、コードを削減、単純化することができます。また PHP でコーディングをする際に OOP の機能を利用すると、よく発生しがちな問題を解決するアルゴリズムとして広く知られた、デザイン・パターンを活用することができます。PHP には V3.0 から OOP 機能が用意されていました。しかし V5.3 が登場するまで、PHP の OOP 実装には少し問題があったため、よく使われているデザイン・パターンのいくつかを使用することができませんでした。PHP V5.3 に遅延静的バインディング (LSB: Late Static Binding) 機能が追加されたことで、そうした問題が解消されています。

この記事では、PHP V5.3 より前のバージョンの PHP で問題のあったデザイン・パターンを紹介し、それらのパターンが機能しなかった理由を説明します。次に、それらの問題が PHP V5.3 の LSB 機能によってどう解決されたのかを説明し、シングルトン・デザイン・パターンとアクティブ・レコード・デザイン・パターンの実装方法を示します。

OOP の復習

これまでに何度か PHP での OOP を扱った経験のある人であれば、下記の 2 つのいずれかの理由から PHP で OOP を行うことをあきらめたのではないでしょうか。

  • PHP での OOP には問題がある、という数多くのブログ・ポストの 1 つを読んだ。
  • 単純なデザイン・パターンを実装しようとしたものの、うまくいかなかった。

PHP が V5.3 となった今、PHP による OOP の問題は大半が解決されていて、OOP に関するブログ・ポストは好意的なものになっており、改めて PHP による OOP を試すには絶好のタイミングです。この記事では、V5.3 が登場する前に問題のあったデザイン・パターンとして、シングルトン、ビルダー、ファクトリー・メソッド、アクティブ・レコードによるデザイン・パターンを検証します。

シングルトン、ビルダー、ファクトリー・メソッドによるデザイン・パターンは、オブジェクトの作成を補助することから、生成に関するパターンと考えることができます。シングルトン・パターンは OOP デザイン・パターンのうち、おそらく最もよく使われているパターンの 1 つになります。このパターンでは、1 つのクラスのオブジェクト・インスタンスは 1 つに制限されます。シングルトンの使い方の例として、データベースのコネクション・プールがあります。つまりアプリケーションは、大量にリソースを消費するコネクション・プール・クラスのインスタンスを複数持つべきではありません。

複雑なオブジェクトの作成と、そのオブジェクトの表示とを分離する必要がある場合には、ビルダー・デザイン・パターンを使用します。ビルダー・デザイン・パターンを使用することで、同じ作成プロセスを使ってさまざまなオブジェクトを作成することができます。ビルダー・デザイン・パターンの実装は複雑になる可能性がありますが、いったんビルダーを用意できると、ビルダーによってオブジェクトの作成が単純化され、またビルダーによって作成されたオブジェクトを使用するのも簡単になります。ビルダーが必要になる一例としては、HTML、XML または PDF を出力する機能を持ったコンバーターがあります。

ファクトリー・メソッド・デザイン・パターンは、名前からわかるように、オブジェクトの生成に使用されるメソッドの実装を定義します。アプリケーションで、オブジェクトのタイプがサブクラスの実装に依存するようなオブジェクトを作成する必要がある場合には、このファクトリー・メソッド・デザイン・パターンを使用します。

アクティブ・レコード・デザイン・パターンは、リレーショナル・データベースの永続化メソッドをドメイン・クラスにラップする場合に使用します。アクティブ・レコード・クラスの各インスタンスはデータベースの特定の行に関連付けられています。アクティブ・レコード・クラスには、データベースに行を挿入するメソッド、データベースから行を削除するメソッド、そしてデータベースの行を更新するメソッドが含まれています。アクティブ・レコード・デザイン・パターンという名前は Martin Fowler が「Patterns of Enterprise Application Architecture」の中で付けたものですが、Ruby on Rails に統合されて使われていることから、よく使われるようになっています。


LSB 登場以前に、生成に関するデザイン・パターンの実装で発生した問題

上で説明した 4 つのデザイン・パターンはすべて、静的な属性とメソッドを使用しています。例えばリスト 1 のコネクション・プールのシングルトンを見てください。

リスト 1. 単純なシングルトン
<?php
class ConnPool { 
    private static $onlyOne;
    private static $count = 0;
    private function __construct() {
	// real-world db conn stuff here...
    } 

    public static function getInstance() { 
        if (!is_object(self::$onlyOne)) { 
	     $klass = __CLASS__;
            self::$onlyOne = new $klass();
            self::$count++;
        } 
        return self::$onlyOne; 
    } 
    public static function getInstanceCount() {return self::$count;}
}  

$db = ConnPool::getInstance();
assert (1 == $db->getInstanceCount());
$db2 = ConnPool::getInstance();
assert (1 == $db2->getInstanceCount());
?>

静的な $onlyOne 変数に注目してください。この変数は、コネクション・プール・オブジェクトのインスタンスを保持するために作られた変数です。$onlyOne 変数には static 修飾子が付けられているため、この変数はクラスそのものに関連付けられています。従って、$onlyOne 変数のスコープはクラス・スコープとなり、クラス属性と呼ばれます。また、$onlyOne 属性のインスタンスは絶対に 1 つしかありません。属性に static 修飾子がない場合、その属性はクラスのインスタンスごとに固有のものとなるため、オブジェクト属性と呼ばれます。

ConnPool のコンストラクター・メソッド (__construct) が空であることに注意してください。本番実装では、このメソッドを使ってデータベース・コネクション・プールの内部構造を作成します。

静的な getInstance メソッドにはシングルトン用のテンプレート・コードが含まれています。getInstance メソッドが $onlyOne のインスタンスを作成するのは、静的な $onlyOne 変数がヌルの場合のみです。getInstance メソッドでは、__CLASS__ というマジック変数を使ってクラスの型を取得し、それからクラスのインスタンスを作成していることに注目してください。

getInstanceCount メソッドをコーディングしている理由は、単にコネクション・プールのインスタンスは絶対に 1 つしか作成されないことを示すためにすぎません。リスト 1 の一番下にある 4 行のコードから、ConnPool プール・クラスのインスタンスを何度要求しても必ず同じオブジェクトが返されることがわかります。

ということは、このシングルトンには何も問題がないということです。つまり、オブジェクト指向の継承ツリーの中にコネクション・プールをサブクラス化し、複数のデータベースをサポートしようとするまでは問題がありません。リスト 2 は継承ツリーを示しています (見やすくするためにインスタンス・カウンターとコンストラクターのコードを削除してあります)。

リスト 2. LSB がサポートされていないため、シングルトンを使おうとしても失敗する例
<?php
class ConnPool { 
    private static $onlyOne;
    protected static $klass = __CLASS__;

    public static function getInstance() { 
        if (!is_object(self::$onlyOne)) { 
            self::$onlyOne = new self::$klass(); 
        } 
        return self::$onlyOne; 
    } 
}  

class ConnPoolAS400 extends ConnPool { 
    protected static $klass = __CLASS__;
}  
$db = ConnPoolAS400::getInstance();
assert ('ConnPoolAS400' == get_class($db)); // fails
?>

シングルトン・クラスの型を複数サポートするために、ConnPool クラスには、サブクラスでオーバーライドされるという前提で、$klass という静的変数が追加されています。そして、ConnPoolAS400 サブクラスは ConnPool クラスを継承し、独自のバージョンの $klass プロパティーを提供しています。つまり、ConnPoolAS400 クラスのインスタンスが作成されると $klass プロパティーには ConnPoolAS400 が保持されることを想定しています。しかしこのコードを実行すると、想定どおりには実行されません。コードの最後にあるアサーションは、PHP のユーティリティー関数である get_classConnPoolAS400 ではなく ConnPool を返した場合には偽になります。問題は、ConnPool クラスの getInstance メソッドが ConnPool クラスの $klass プロパティーを使用するようになっており、オーバーライドされた ConnPoolAS400 クラスの getInstance メソッドでも、ConnPoolAS400 クラスの $klass プロパティーではなく、ConnPool クラスの $klass プロパティーが使用されることにあります。

リスト 2 のコードを実行して経験した問題は、コンパイル時に self キーワードは参照されたプロパティーまたはメソッドにバインドされるという点にあります。self キーワードは self を包含するクラスを指しており、サブクラスを認識していません。基本的に、コンパイラーは self キーワードを包含するクラスの名前で self キーワードを置き換えます。例えば下記のようなコードの場合、

self::$onlyOne = new self::$klass();

コンパイラーによって置き換えられた結果、下記のようになります。

ConnPool::$onlyOne = new ConnPool::$klass();

これは言うなれば、「早期バインディング」ですが、ここで必要なのは遅延バインディングです。


LSB によってシングルトンを継承する

シングルトンの問題は PHP V5.3 の LSB 機能を使って解決することができます。以下のように単純に self::$onlyOne = new self::$klass(); の右辺にある self 指定子を static で置き換えます。

self::$onlyOne = new static::$klass();

そして先ほどのコードを再度実行すると、アサーションが真になります。

static キーワードにより、PHP は可能な限り後になってからコード実装へのバインドを行います。LSB を使用しない場合には、self::$klass は最初に発見されるコード、つまり親クラスのバージョンのコードを参照します。

PHP V5.3 では、get_called_class という新しい関数によって、シングルトンのコードが少し単純化されます。リスト 3 のコードは、静的な $klass 属性を使用する代わりに get_called_class 関数を使用しています。

リスト 3. get_called_class を使用してシングルトンを単純化する
<?php
class ConnPool { 
	private static $instance;
	public function get_instance() {
		if (!is_object(self::$instance)) {
			$klass = get_called_class();
			self::$instance = new $klass();
		}
		return self::$instance;
	}
}  

class ConnPoolAS400 extends ConnPool {}  
$db = ConnPoolAS400::get_instance();
assert ('ConnPoolAS400' == get_class($db));
?>

リスト 3 のシングルトンは確かに従来よりも簡潔ですが、重要な点はオーバーライドされた静的属性を、PHP の LSB を使用して参照している点です。シングルトンの実装ではサブクラスのクラス名を使用しますが、他のパターン (後ほど説明するアクティブ・レコード・パターンなど) では他の静的プロパティーを参照する必要があります。また、LSB は静的属性で機能するだけではなく、静的関数でも機能します。静的関数は、静的プロパティーと同様にクラス・スコープであり、そのクラスのオブジェクト・インスタンスをスコープとしているわけではありません。リスト 4 に示すシングルトンは、属性の代わりにメソッドを使用して適切なクラスを指定しています。

リスト 4. メソッドで LSB を使用するシングルトン
<?php
class ConnPool { 
	private static $onlyOne;
	protected static function getClass() {
		return __CLASS__;
	}

	public function get_instance() {
		if (!is_object(self::$onlyOne)) {
			$klass = static::getClass();
			self::$onlyOne = new $klass();
		}
		return self::$onlyOne;
	}
}  

class ConnPoolAS400 extends ConnPool { 
	protected static function getClass() {
		return __CLASS__;
	}
}  
$db = ConnPoolAS400::get_instance();
assert ('ConnPoolAS400' == get_class($db));
?>

このコードでは、getClass の静的な実装が ConnPool の中で定義され、ConnPool400 の中でオーバーライドされています。ConnPoolget_instance メソッドの中にある下記の行により、適切なメソッドが実行時に呼び出されます。

$klass = static::getClass();

アクティブ・レコード

アクティブ・レコード・デザイン・パターンの単純な (そして部分的な) 実装を調べてみましょう。リスト 5 は、ActiveRecord という抽象クラス、そして CustomerSales という 2 つのサブクラスを示しています。この 2 つのサブクラスはアプリケーション・ドメインに存在するエンティティーへのラッパーを提供するため、ドメイン・クラスと呼ばれます。

リスト 5. アクティブ・レコード・デザイン・パターンの単純な実装
<?php
abstract class ActiveRecord {
  protected static $table;
  protected $fieldvalues;
  public $select; // used for illustration only 

  static function findById($id) {
    $query = "select * from "
        .static::$table
        ." where id=$id";
    return self::createDomain($query);
  }
  function __get($fieldname) {
    return $this->fieldvalues[$fieldname];
  }
  static function __callStatic($method, $args) {
    $field = preg_replace('/^findBy(\w*)$/', '${1}', $method);
    $query = "select * from "
        .static::$table
        ." where $field='$args[0]'";
    return self::createDomain($query);
  }
  // TODO: code a __set method
  private static function createDomain($query) {
    $klass = get_called_class();
    $domain = new $klass();
    $domain->fieldvalues = array();
    $domain->select = $query;
    foreach($klass::$fields as $field => $type) {
      $domain->fieldvalues[$field] = 'TODO: set from sql result';
    }
    return $domain;
  }
  // TODO: code static create, update, delete methods
}
class Customer extends ActiveRecord {
  protected static $table = 'custdb';
  protected static $fields = array(
    'id' => 'int',
    'email' => 'varchar',
    'lastname' => 'varchar'
    );
}
class Sales extends ActiveRecord {
  protected static $table = 'salesdb';
  protected static $fields = array(
    'id' => 'int',
    'item' => 'varchar',
    'qty' => 'int'
    );
}

assert ("select * from custdb where id=123" == 
        Customer::findById(123)->select);
assert ("TODO: set from sql result" == 
    Customer::findById(123)->email);
assert ("select * from salesdb where id=321" == 
        Sales::findById(321)->select);
assert ("select * from custdb where Lastname='Denoncourt'" == 
    Customer::findByLastname('Denoncourt')->select);
?>

ActiveRecord クラスは abstract 修飾子を使用し、ActiveRecord オブジェクトをインスタンス化できないようにしています。new ActiveRecord(); を使って ActiveRecord を作成しようとするとエラーが発生し、「PHP Fatal error: Cannot instantiate abstract class ActiveRecord. (PHP 致命的エラー: 抽象クラス ActiveRecord をインスタンス化することはできません)」と表示されます。ActiveRecord クラスは、サブクラス化しない限りは実質的に何もしないので、こうしたエラーが表示されるのは適切なことです。

ActiveRecord クラスは静的な $table 変数を定義します。この $table 変数は後で、custdbsalesdb という SQL テーブル名を指定する Customer サブクラスと Sales サブクラスによってオーバーライドされます。

ActiveRecord の静的な findById 関数は、アクティブ・レコード・デザイン・パターンの実装によく見られるメソッドの例です。findById は、渡された一意の ID に基づいてデータベースの適切な行を取得した後、ビジネス・エンティティーを表現するドメイン・オブジェクトを作成して返すメソッドです。このメソッドでは static キーワードを使用することで、サブクラスのテーブル名を遅延バインディングによって参照できるようにします。また、SQL の select 文を作成した後、ドメインの作成を createDomain メソッドまで遅らせます。

createDomain メソッドは (PHP V5.3 の get_called_class 関数を利用して) サブクラスの名前を使用し、適切なクラスをインスタンス化します。次に createDomain メソッドは、データベースの列の名前と値のマップを保持する配列を作成します。ここでは例を単純にするために、ActiveRecord は実際には SQL コードを実行していません。また、この記事のコードによって SQL の select 文の作成方法を説明、テストできるように、ActiveRecord には createDomain に設定された $select プロパティーがあります。foreach 文は SQL の結果セットからドメイン属性の値を設定する代わりに、フィールド値の配列の各要素に “TODO: set from sql result” というストリングを格納します。createDomain メソッドは新たに作成されたドメインを返し、そのドメインが今度は findById メソッドによって返されます。このコードの最後にある 4 つのアサーションのうち、最初のアサーションによって、適切な SQL 文が作成されたかどうかの検証を行っています。


動的属性と __get

皆さんは、CustomerSales というドメイン・クラスに関して 1 つ奇妙なことにお気付きなのではないでしょうか。この 2 つのクラスにはドメイン属性がないように見えますが、皆さんは Customer クラスのプロパティーとして、例えば以下のような行があると予想していたのではないでしょうか。

$id;
$email;
$name;

上記の行があれば、以下のようにして、これらのドメイン・プロパティーにアクセスすることができます。

$custObj->id;
$custObj->email;
$custObj->lastname;

しかし、Customer クラスのプロパティーのコードがなくても、これらの属性は $fieldvalues 配列に保持されていて使用することができるため、上記のコードは問題なく機能します。ドメイン・プロパティーにシームレスにアクセスでき、上記の参照構文が機能するように、ActiveRecord には __get というマジック・メソッドが実装されています。コードの最後にある 4 つのアサーションのうち、2 番目のアサーションは、findById メソッドから Customer オブジェクトを取得する方法と、email プロパティーにアクセスする方法を示しています。Customer には email プロパティーがありませんが、__get メソッドが定義されているため、PHP は __get メソッドを呼び出し、要求されたプロパティーの名前をパラメーターとして渡します。次に __get メソッドは単純に $fieldvalues 配列から属性の名前を取得します。

注: 本番コードでは、配列に存在しない属性に対するリクエストを処理する必要があります。


動的なファインダー・メソッドと __callStatic

アクティブ・レコード・デザイン・パターンの実装に含まれていることが多い優れた機能として、動的なファインダー・メソッドがあります。動的なメソッドがない場合、ドメイン・クラスをコーディングする人は、そのクラスのデータベース・クエリーを使用する人全員が 1 つのドメインのインスタンスを 1 つまたは複数取得する (そして findById のようなメソッドをコーディングする) 必要があるかもしれない、と考えなければなりません。

リスト 5 の最後にある 4 つのアサーションのうち、一番最後のアサーションを見てください。このアサーションは findByLastname というメソッドを実行しますが、このメソッドには実装がありません。このメソッドは存在しないかもしれませんが、Customer クラスの親クラスには PHP V5.3 の新しい __callStatic というマジック・メソッドの実装があるため、エラーがスローされないだけではなく、findByLastname の呼び出しが実際に実行され、何らかの処理が行われます。

__callStatic メソッドは、呼び出されるメソッドの名前、そして引数の配列、という 2 つのパラメーターを引数に取ります。findByLastname が呼び出されると、PHP はその名前が存在しないことを認識して ActiveRecord__callStatic メソッドを実行し、1 番目のパラメーターとして findByLastname を、2 番目のパラメーターとして Denoncourt という配列を渡します。ActiveRecord__callStatic の実装では、findBy 接頭辞の後に続くストリングを抽出して、そのストリングを SQL の where 節のフィールド名として使用します。次に __callStatic メソッドは Denoncourt 引数を比較の値として使用します。動的な呼び出しを利用できるため、以下のようなコードを使用することもできます。

Customer::findByEmail('dondenoncourt@gmail.com');

また、演算子をサポートするように __callStatic メソッドを機能強化することもできます。下記はその一例です。

Sales::findAllByQtyGreaterThan(100);

当然のことですが、アクティブ・レコードを本番実装する場合には、これよりもはるかに高度なはずであり、findBy 以外のメソッド接頭辞をいくつも処理することになるでしょう。例えばメソッド接頭辞として、行の配列を返す findAllBy や、ある基準を満たす行の数を返す countBy などが考えられます。

注: PHP V5.3 の新機能を利用するアクティブ・レコード・フレームワークが既にいくつか登場しています (Dirivante や php.activerecord など、「参考文献」を参照)。


まとめ

1970年代に育った私たちは、static を使った「Don't give me no static (喧嘩上等)」というスラングをよく使ったものです。嬉しいことに、PHP V5.3 でも static が大量に使えるようになりました (例えば、継承ツリーでの静的 (static) なプロパティーや静的 (static) なメソッドなど)。PHP V5.3 の LSB 機能によって、静的なプロパティーや静的なメソッドを必要とするデザイン・パターンを使用できるようになったのです。また PHP には get_called_class も用意されています。デザイン・パターンを実装する場合、派生クラスのクラス名が必要になることが多いため、皆さんは get_called_class を頻繁に使うことになるはずです。さらに PHP V5.3 のマジック関数 __callStatic を利用すると、これまでよりも創造性を発揮できるようになります。


ダウンロード

内容ファイル名サイズ
Sample PHP scriptsos-php-53static-latestaticbinding_denoncourt.zip34KB

参考文献

学ぶために

製品や技術を入手するために

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=632026
ArticleTitle=PHP V5.3 で遅延静的バインディングを使ったオブジェクト指向プログラミングを活用する
publish-date=02152011