オブジェクト指向プログラミング (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 機能によってどう解決されたのかを説明し、シングルトン・デザイン・パターンとアクティブ・レコード・デザイン・パターンの実装方法を示します。
これまでに何度か 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_class が ConnPoolAS400 ではなく 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(); |
これは言うなれば、「早期バインディング」ですが、ここで必要なのは遅延バインディングです。
シングルトンの問題は 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 の中でオーバーライドされています。ConnPool の get_instance メソッドの中にある下記の行により、適切なメソッドが実行時に呼び出されます。
$klass = static::getClass(); |
アクティブ・レコード・デザイン・パターンの単純な (そして部分的な) 実装を調べてみましょう。リスト 5 は、ActiveRecord という抽象クラス、そして Customer と Sales という 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 変数は後で、custdb と salesdb という 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 文が作成されたかどうかの検証を行っています。
皆さんは、Customer と Sales
というドメイン・クラスに関して 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 配列から属性の名前を取得します。
注: 本番コードでは、配列に存在しない属性に対するリクエストを処理する必要があります。
アクティブ・レコード・デザイン・パターンの実装に含まれていることが多い優れた機能として、動的なファインダー・メソッドがあります。動的なメソッドがない場合、ドメイン・クラスをコーディングする人は、そのクラスのデータベース・クエリーを使用する人全員が
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 scripts | os-php-53static-latestaticbinding_denoncourt.zip | 34KB | HTTP |
学ぶために
- ウィキペディアには、生成に関するデザイン・パターンが適切に説明されており、その背景についても説明されています。
- PHP.net には PHP
開発者のためのリソースが集まっています。
- 「Recommended
PHP reading list」を調べてみてください。
- developerWorks には他にも PHP
に関する記事が豊富に用意されています。
- developerWorks を Twitter でフォローしてください。
- IBM developerWorks の PHP project
resources を利用して PHP のスキルを磨いてください。
- developerWorks podcasts
ではソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。
- PHP でデータベースを使うのであれば、Zend Core for IBM
を調べてみてください。これはシームレスでそのまま使用でき、インストールも容易な、IBM DB2 V9 をサポートする PHP の開発環境であり本番環境でもあります。
- developerWorks の
Technical events and webcasts で最新情報を入手してください。
- IBM
オープンソース開発者にとって関心のある、世界中で今後開催される会議や業界展示会、ウェブキャスト、その他のイベントについて調べてみてください。
- developerWorks の Open source
ゾーンをご覧ください。オープンソース技術を使った開発や、IBM
製品でオープンソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報など、豊富な情報が用意されています。また最も人気のあった記事やチュートリアルもご覧ください。
- IBM とオープンソース技術、そして製品機能を調べ、学ぶために、無料の developerWorks On demand
demos をご覧ください。
製品や技術を入手するために
- PHP V5.3 の新機能を利用する Dirivante と php.activerecord について学んでください。
- 皆さんの次期オープンソース開発プロジェクトを IBM ソフトウェアの試用版を使って革新してください。ダウンロード、あるいは DVD
で入手することができます。
- IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox
のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、WebSphere®
などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。
議論するために
- developerWorks
コミュニティーで開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとやり取りしてください。
- developerWorks
blogs から developerWorks のコミュニティーに加わってください。
- developerWorks
の PHP Forum: Developing PHP applications with IBM Information Management products
(DB2, IDS) に加わってください。
