PHPでなんちゃってMixinを実現してみる

todaDRY原則,mixin,PHP,なんちゃって,ダイヤモンド継承,プラグイン,多重継承

秋も深くなってきました。秋の夜長いかがお過ごしですか?
今週末はTOEIC試験を控えていますが、何にも勉強はしておりません。とりあえず、自分の英語力がどれだけ低いのかを確認してきたいと思います。こんにちわ。todaです。

最近、まつもとゆきひろさんご著書の「まつもとゆきひろ コードの世界」を拝読しました。rubyの設計・開発を手掛けた方だけあって、言語についての深い知識と考察や歴史が学べて面白かったです。

今回は、その中で気を引いた、"mixin"という多重継承のスマートな使い方をPHPで実現できないかという実験をしてみます。

初めから出鼻をくじきますが、mixinをPHPでそのままの形で実現することはできません。mixinはあくまでも多重継承のスマートな使い方であり、PHPは多重継承がサポートされていないからです。

最初にmixinの説明をします。これはクラスの継承関係において意味的なis-a関係と、機能的なis-a関係を分離するための手法です。

意味的なis-a関係を主にして、それぞれのクラスで必要な機能は、その機能を受け持つクラス(mixinクラス)から継承して取り込みます。ちょうど、mixinクラスをプラグインのようなものとして、本体に取り込む感じです。継承によって取り込みますので、必然的に多重継承となります。

具体的かつ、典型的な例として、(標準入出力などの)ストリームを表すStreamクラスを考えます。
このStreamクラスでは、入力の機能を持つInStreamクラスと、出力の機能を持つOutStreamクラス、入出力の機能をもつInoutStreamクラスの3つのサブクラスを考えます。

mixinを使わない旧来の継承方法では、このような継承関係になります。
image1

InoutStream is-a InStream. InoutStream is-a OutStreamですから、とても自然な継承関係です。InoutStreamクラスはInStream, OutStreamの両方を継承することでお互いの機能を取り込みます。

これはいわゆるダイヤモンド継承の形であり、望ましくない多重継承の形といわれています。なぜなら、InoutStreamがInStreamとOutStreamの両方を継承していることにより、InoutStreamはStreamの部分を重複して持つことになるからです。これは以下のような複雑な問題を抱えています。
1. InStreamとOutStreamで同じメソッド・プロパティが存在した場合、どちらが有効になるのか。
2. InoutStreamはStreamの部分を重複して持つので、それらを識別する必要がある。

このクラス構造をmixinを使った継承関係に直すと、継承関係はこうなります。
image2

意味的なis-a関係はInStream, OutStream, InoutStreamクラスとStreamクラスの継承関係です。これが主の継承関係となります。

実際の入力、出力の機能は色を付けたReadableStream, WritableStreamクラスが受け持っています。これらをプラグインとして、InStream, OutStream, InoutStreamがそれぞれ必要な機能を取り込み(mixin)ます。

ReadableStream, WritableStreamが継承によって取り込まれているのには重要な利点があります。それは、継承によって取り込むことにより、ReadableStream, WritableStreamで定義されている実装の共有が可能なることです。同じコードは一か所にまとめるというプログラミングの大鉄則であるDRY(Don’t Repeat Yourself: 同じことを繰り返すな)の原則を守ることができます。

言語的に多重継承がサポートされていないPHPやJavaでは、意味的なis-a関係をクラス間の継承、機能的なis-a関係をインターフェイスの実装という形で同様の継承関係を構築しなければなりません。

しかし、インターフェイスは仕様の宣言にすぎず、実装は含まれていません。それぞれInStreamクラス, OutStreamクラス, InoutStreamクラスでReadableStream, WritableStreamインターフェイスの実装を定義する必要があります。

image4

つまり、InStream, InoutStreamクラスそれぞれでReadableStreamインターフェイスを実装しなければいけません。この場合両者は同じコードになりますので、DRY原則に反することになります。

mixinというスマートな多重継承の使い方によって、ダイヤモンド継承の複雑さを回避しつつ、DRY原則も守ることができます。非常に優れたパターンといえます。

本題に入ります。PHPでmixinそのものは無理ですので、なんちゃってmixinを実現します。最終的なクラス構造はこのようになります。

image3

GoFのStrategyパターンの変種のようなものです。ベースとなるStreamクラス(のサブクラス)の生成時に、プラグインとなるクラス(ReadableStream, WritableStream)のインスタンスを結合し、プラグインのメソッド呼び出しは、Streamクラスから処理を委譲することにします。Strategyパターンとの違いは、複数のプラグインを結合することができることです。

最初に実際の機能を提供するReableStream, WritableStreamを定義します。今回はMixinクラスであることを明示するため、クラス名をReadableStreamMixin, WritableStreamMixinとし、両者の親クラスStreamMixinを置きます。

abstract class StreamMixin
{
	// コンストラクタ等は省略。以下同じ。
}

class ReadableStreamMixin extends StreamMixin
{
	public function readData($size)
	{
		echo "$size byte Data reading...\n";
	}
}

class WritableStreamMixin extends StreamMixin
{
	public function writeData($data)
	{
		echo "Data '$data' writing...\n";
	}
}

次に本体となるInStream, OutStream, InoutStreamクラスの共通の親クラスとなるStreamクラスを定義します。

abstract class Stream
{
	private $_methods = array();

	public function __construct()
	{
	}

	protected function _addMixin(StreamMixin $mixin)
	{
		$reflection = new ReflectionObject($mixin);
		$methods = $reflection->getMethods();
		foreach($methods as $method){
			$method_name = $method->getName();

			if($method->isConstructor() || $method->isDestructor()){
				continue;
			}
			if($method->isPublic() != true){
				continue;
			}

			if(array_key_exists($method_name, $this->_methods)){
				throw new Exception("同名のMixinメソッドが登録されています。: $method_name");
			}

			$this->_methods[$method_name] = $mixin;
		}
	}

	public function __call($method_name, $args)
	{
		if(array_key_exists($method_name, $this->_methods) != true){
			throw new Exception("指定されたMixinメソッドは登録されていません。: $method_name");
		}

		$mixin = $this->_methods[$method_name];
		call_user_func_array(array($mixin, $method_name), $args);
	}
}

今回の肝となる_addMixin()メソッドを用意しています。この_addMixin()メソッドはmixinするStreamMixinクラスのインスタンスを受け取ります。そのStreamMixクラスで定義されているpublicメソッド(コンストラクタ, デストラクタ除く)を取得してして、内部の$this->_methods配列にメソッド名と、インスタンスの対応表を記録します。

StreamMixinクラスへのメソッドの委譲は、Streamクラスに定義したマジックメソッド__call()を経由で行います。メソッド名に対応するインスタンスを$this->_methods配列から取得し、そのインスタンスのメソッドをcall_user_func_array()関数で呼び出します。

最後にStreamクラスのサブクラスInStream, OutStream, InoutStreamクラスを定義します。それぞれのコンストラクタで必要なStreamMixinクラスを_addMixin()で追加すれば良いのです。

class InStream extends Stream
{
	public function __construct()
	{
		$this->_addMixin(new ReadableStreamMixin());
	}
}

class OutStream extends Stream
{
	public function __construct()
	{
		$this->_addMixin(new WritableStreamMixin());
	}
}

class InoutStream extends Stream
{
	public function __construct()
	{
		$this->_addMixin(new ReadableStreamMixin());
		$this->_addMixin(new WritableStreamMixin());
	}
}

クラスの利用者はこのような裏事情を気にすることはありません。それぞれのInStream, OutStream, InoutStreamクラスに直接定義されているメソッドとして呼び出すことが可能です。

$in_stream = new InStream();
$in_stream->readData(15);
// 15 byte Data reading...

$out_stream = new OutStream();
$out_stream->writeData("0123456789");
//  Data '0123456789' writing ...

$inout_stream = new InoutStream();
$inout_stream->readData(30);
// 30 byte Data reading...
$inout_stream->writeData("abcdefghij");
//  Data 'abcdefghij' writing ...

「なんちゃって」ではありますが、委譲をうまく利用することでmixin相当のクラスを構築することができることが分かります。この設計をさらに発展させて、_addMixin()されていないメソッドについて、デフォルトの振る舞いを定義(今回は例外をthrow)できるようになると、より実用的になると思います。