インスタンス生成判定問題
インスタンス生成に失敗する可能性があるクラスの実装を迫られる時ってありますよね。
しかし、PHPのコンストラクタの戻り値は自信のインスタンスを必ず返すため、例外をスローする以外の方法ではエラーハンドリングがやりづらい!
2つに分けて段階的に対策のメモ書き。
- 一般的な対策
- トレイトを用いた対策
※コンストラクタで例外をスローして、生成側でキャッチするのが一番綺麗な形ではありますが、
try-catchを入れたくない場合や、例外投げるクラスとの区別をつける場合などに有効な対策になります。
※PHP5.6で動作確認を行ったソースになります
一般的な対策
例が微妙ですが、ここでは簡単にするために、ただ単に数字を保持するだけのクラスを考えます。
数字以外がコンストラクタの引数に来たらそもそもこのクラスのインスタンスは使い物にならないとすると、
一般的には以下のように対策をするかと思います。
インスタンス生成判定ができる数値保持クラス
<?php
class NumberContainer
{
private $num;
public function getNumber()
{
return $this->num;
}
/**
* newでのインスタンス生成を禁止
*/
final private function __construct() {}
/**
* インスタンスの生成
* @param int $num
* @return self|null
*/
final public static function create($num)
{
// 自分自身のインスタンス生成
$instance = new self();
// インスタンス初期化メソッド呼び出し、失敗時はnullを返す
return $instance->init($num) ? $instance : null;
}
/**
* インスタンス初期化処理
* @param int $num
* @return bool
*/
private function init($num)
{
// 数字以外の場合インスタンス生成失敗
if (!is_numeric($num)) return false;
$this->num = $num;
return true; // インスタンス生成成功
}
}
インスタンス生成判定可能クラスの解説
__construct()
new NumberContainer($num)
で初期化ができないように、__construct()をprivateにしてfinalでオーバーライドを禁止する。
これで子クラスからもnewを使ってのインスタンス生成は不可能になる。
create()
この関数を経由してインスタンスを生成することになる。
インスタンスが存在しない状態で呼び出せるようにstatic。
インスタンス生成が成功すればインスタンスを返して、なければnullを返す。
init()
インスタンスの初期化処理部分、本来であればコンストラクタにあたる処理。
生成に成功すればtrue, 失敗すればfalseを返す。
利用例
以下のスクリプトで、実際に実行してみます。
<?php
require_once('NumberContainer.php');
// 成功予定のインスタンス生成
$arg1 = '111';
$instance1 = NumberContainer::create($arg1);
checkInstance($arg1, $instance1);
// 失敗予定のインスタンス生成
$arg2 = 'aaa';
$instance2 = NumberContainer::create($arg2);
checkInstance($arg2, $instance2);
// チェック用関数
function checkInstance($arg, $instance)
{
echo $arg . ' >> ';
if (!is_null($instance)) {
echo 'SUCCESS (' . $instance->getNumber() . ')';
} else {
echo 'FAILURE';
}
echo PHP_EOL;
}
111 >> SUCCESS (111)
aaa >> FAILURE
これで解決ではありますが、このような方法で利用したいインスタンスが複数出て来た場合に、毎回毎回同じような処理書くのめんどくさい!それに、コーディングする人によっては関数名などの表現が変わる可能性があるのはよろしくないですよね...。
トレイトを用いた対策
そこで、このインスタンス生成判定ができる振る舞いをトレイトで用意してあげれば、便利になるのでは!?と思ったので、早速やってみます!
インスタンス生成判定用トレイト
<?php
trait InstanceCreator
{
/**
* newでのインスタンス生成を禁止
*/
final private function __construct() {}
/**
* インスタンス初期化メソッド実装の強制
* @param ...$args 任意の型と数の引数を利用できる
* @return bool
*/
abstract protected function init(...$args);
/**
* インスタンスの生成
* @param ...$args 任意の型と数の引数を利用できる
* @return self|null 生成成功:self, 失敗:null
*/
final public static function create(...$args)
{
// 自分自身のインスタンス生成
$instance = new self();
// ReflectionMethodの準備
$reflectedMethod = new ReflectionMethod(__CLASS__, 'init');
$reflectedMethod->setAccessible(true);
// インスタンス初期化メソッド引数の数チェック
$argsCount = count($args);
if ($argsCount < $reflectedMethod->getNumberOfRequiredParameters()
|| $argsCount > $reflectedMethod->getNumberOfParameters()) {
error_log('[' . __CLASS__ . '::create] Count of args is invalid.');
return null;
}
// インスタンス初期化メソッド呼び出し、失敗時はnull
return $reflectedMethod->invokeArgs($instance, $args) ? $instance : null;
}
}
インスタンス生成判定トレイトの解説
基本的な考えは変わらないが、ポイントは以下の通り
- __construct()がprivateでオーバーライド不可
- init()が抽象化されているので、実装を強制される
- create(), init()が可変引数に対応
- initの引数の個数とcreateで渡された引数の個数があっていなければエラーとなる
initメソッドでデフォルト引数を利用する場合に対応できてません!
いい案あればご教授いただけると幸いです。
@nunulkさんよりアドバイスいただき、
ReflectionMethod::getNumberOfRequiredParameters()を利用して、
デフォルト引数問題を解決でき、スッキリとしたコードになりました!
ありがとうございます!
トレイト(InstanceCreator)を用いたインスタンス生成判定可能クラス
これを利用したNumberContainerクラスを作ってみるとこんな感じになります。
<?php
require_once('InstanceCreator.php');
class NumberContainer
{
use InstanceCreator;
private $num;
public function getNumber()
{
return $this->num;
}
/**
* インスタンス初期化処理
* @param int $num
* @return bool
*/
private function init($num)
{
// 数字以外の場合インスタンス生成失敗
if (!is_numeric($num)) return false;
$this->num = $num;
return true; // インスタンス生成成功
}
}
かなりスッキリしましたね!
これで、InstanceCreatorトレイトを利用したクラスは、冗長なcreate()と__construct()は記述する必要がなくなり、initメソッドを実装しないとエラーになります。
利用例
これで先ほどと同じ、スクリプトで実行してみると...
<?php
require_once('NumberContainer.php');
// 成功予定のインスタンス生成
$arg1 = '111';
$instance1 = NumberContainer::create($arg1);
checkInstance($arg1, $instance1);
// 失敗予定のインスタンス生成
$arg2 = 'aaa';
$instance2 = NumberContainer::create($arg2);
checkInstance($arg2, $instance2);
// チェック用関数
function checkInstance($arg, $instance)
{
echo $arg . ' >> ';
if (!is_null($instance)) {
echo 'SUCCESS (' . $instance->getNumber() . ')';
} else {
echo 'FAILURE';
}
echo PHP_EOL;
}
111 >> SUCCESS (111)
aaa >> FAILURE
先ほどと同じ結果が得られました!
これなら、汎用的に使えそうです。
PHPのトレイトはメタプログラミング的にも使えたりして便利ですねー