PHP

phpでクラスをメモ化するtrait


メモ化 is 何

メモ化は高速化技法の1つである。

phperなら配列に関数/メソッドの戻り値をキャッシュしておいたことがあると思う。

この類のキャッシュはアプリケーションロジックと混ざると扱いが難しくなり

バグを生むことがある。またキャッシュするロジック自体はボイラープレートだ。

どうにかDRYかつ疎結合にメモ化ができないかと思い、traitと無名クラスを使う方法を考えた。


利用シーン

処理が遅いメソッドを持つクラスがあったとする。

# 定義

class PoorClass {
function slowProc($arg) {
sleep(1);
return $arg + 1;
}
}

# 利用コード
$obj = new PoorClass;
for ($i = 0; $i < 5; $i++) {
var_dump($obj->slowProc(1));
}

この利用コードは必ず5秒かかる。かかる部分を短縮したいとしよう。

そこで、こんなtraitを用意する。

trait Cacheable {

public function __call($name, $arguments)
{
static $cache = [];

if (preg_match('/^(.*)WithCache$/', $name, $matches)) {
$real_name = $matches[1];

$key = $real_name.'('.serialize($arguments).')';

if (isset($cache[$key])) {
return $cache[$key];
}

if (method_exists($this, $real_name)) {
return $cache[$key] = call_user_func_array([$this, $real_name], $arguments);
}
}

throw new BadMethodCallException();
}
}

利用コードを次のように改変する。

$obj = new class extends PoorClass {

use Cacheable;
};

for ($i = 0; $i < 5; $i++) {
var_dump($obj->slowProcWithCache(1));
}

ポイントは2つ。


  1. php7の無名クラスを使ってオブジェクト生成時にtraitをmixinする

  2. 呼び出しメソッド名をtraitでフックされるよう変える

これにより slowProc の結果がキャッシュされほぼ1秒で処理できるようになった。


Pros & Cons

この手法の良いところ・悪いところをまとめる。


  • 良いところ


    • キャッシュのロジックがDRYになる。アプリケーションロジックと分離できる。

    • インスタンス単位でキャッシュを持つのでキャッシュの管理が楽。不要になればインスタンスを破棄すればいい。

    • 元のクラスのサブクラスなので、type hintしてる引数や型の判定してるとこにも使える



  • 良くないところ


    • 呼び出しコード(メソッド名)が変わってしまう

    • IntelliJとかのチェックでは slowProcWithCache が存在しない扱いになる。 @method アノテーションもオブジェクトには効かないっぽい




所感


  • 元のクラスのサブクラスで、

  • メソッド名を変えずに、

  • かつDRYにキャッシュロジックを注入する方法

を考えたけど自分の力ではムリだった。

良い方法があれば知りたい。


追記(2019.2.9)

この記事を見た同僚の @eielh にコメントをもらった。


  • メモ化は必要条件として、純粋関数であるという前提がある

  • ので、メソッド全部メモ化したいという要求は普通はない

  • ので、メソッドは一つ一つ実装すれば、メソッド名も変わらないしIDEの支援もうけられる

  • ので、(編注:素直な実装をすることは)妥当な妥協なのではないか

もっともだと思ったので、一例として愚直に書く例を載せておきたい。

# 継承して愚直に書く

class CachedPoorClass extends PoorClass {
function slowProc($arg) {
static $cache = [];
$key = serialize($arg);
if (isset($cache[$key])) {
return $cache[$key];
}
return $cache[$key] = parent::slowProc($arg);
}
}

これだと、キャッシュのロジックがWETになるのを妥協する代わりに以下のものを得られる。


  • 元のクラスのサブクラスで

  • メソッド名を変えずに

  • IDEの支援も普通に受けつつ

  • 影響範囲を局所化できる

コメントに補足がたくさんあるので参照されたい。