在PHP中实现方法结果缓存的装饰器模式的最佳方式


Best way to implement a decorator pattern for method result caching in PHP

我有一组类,它们习惯于用相同的参数重复调用。这些方法通常运行数据库请求和构建对象数组等,因此为了消除这种重复,我构建了两种缓存方法来优化。这些是这样使用的:

应用缓存之前:

public function method($arg1, $arg2) {
$result = doWork();
return $result;
}

应用缓存后:

public function method($arg1, $arg2, $useCached=true) {
if ($useCached) {return $this->tryCache();}
$result = doWork();
return $this->cache($result);
}

不幸的是,我现在要手动将其添加到所有方法中,这项任务有点费力——我相信这是decorator模式的一个用例,但我不知道如何在PHP中以更简单的方式实现它。

最好的方法是什么?希望这些类中的所有方法都能自动做到这一点,或者我只需要在方法中添加一行等等?

我已经研究了覆盖return语句之类的方法,但实际上什么都看不到。

谢谢!

如果您不需要类型安全,您可以使用通用的缓存装饰器:

class Cached
{
    public function __construct($instance, $cacheDir = null)
    {
        $this->instance = $instance;
        $this->cacheDir = $cacheDir === null ? sys_get_temp_dir() : $cacheDir;
    }
    public function defineCachingForMethod($method, $timeToLive) 
    {
        $this->methods[$method] = $timeToLive;
    }
    public function __call($method, $args)
    {
        if ($this->hasActiveCacheForMethod($method, $args)) {
            return $this->getCachedMethodCall($method, $args);
        } else {
            return $this->cacheAndReturnMethodCall($method, $args);
        }
    }
    // … followed by private methods implementing the caching

然后,您可以将一个需要缓存的实例包装到这个Decorator中,如下所示:

$cachedInstance = new Cached(new Instance);
$cachedInstance->defineCachingForMethod('foo', 3600);

显然,$cachedInstance没有foo()方法。这里的技巧是利用神奇的__call方法来拦截对不可访问或不存在方法的所有调用,并将它们委托给装饰实例。通过这种方式,我们通过Decorator公开装饰实例的整个公共API。

如您所见,__call方法还包含用于检查是否为该方法定义了缓存的代码。如果是,它将返回缓存的方法调用。如果没有,它将调用实例并缓存返回。

或者,将专用的CacheBackend传递给Decorator,而不是在Decorator本身中实现Caching。然后,Decorator将仅作为装饰实例和后端之间的中介器工作。

这种通用方法的缺点是缓存装饰器将不具有装饰实例的类型。当您的消费代码需要Instance类型的实例时,您将收到错误。


如果您需要类型安全的装饰器,则需要使用"经典"方法:

  1. 创建装饰实例公共API的接口。你可以手动完成,或者,如果工作量很大,可以使用我的Interface Distiller)
  2. 更改每个方法上的TypeHints,这些方法需要接口的装饰实例
  3. 让Decorated实例实现它
  4. 让Decorator实现它,并将任何方法委托给装饰实例
  5. 修改所有需要缓存的方法
  6. 对所有要使用decorator的类重复此操作

简而言之,

class CachedInstance implements InstanceInterface
{
    public function __construct($instance, $cachingBackend)
    {
        // assign to properties
    }
    public function foo()
    {
        // check cachingBackend whether we need to delegate call to $instance
    }
}

的缺点是工作量更大。对于每个应该使用缓存的类,都需要这样做。您还需要对每个函数的缓存后端进行检查(代码重复),并将任何不需要缓存的调用委托给修饰的实例(乏味且容易出错)。

使用__call魔术方法

class Cachable {
    private $Cache = array();
    public function Method1(){
        return gmstrftime('%Y-%m-%d %H:%M:%S GMT');
    }
    public function __call($Method, array $Arguments){
        // Only 'Cached' or '_Cached' trailing methods are accepted
        if(!preg_match('~^(.+)_?Cached?$~i', $Method, $Matches)){
            trigger_error('Illegal Cached method.', E_USER_WARNING);
            return null;
        }
        // The non 'Cached' or '_Cached' trailing method must exist
        $NotCachedMethod = $Matches[1];
        if(!method_exists($this, $NotCachedMethod)){
            trigger_error('Cached method not found.', E_USER_WARNING);
            return null;
        }
        // Rebuild if cache does not exist or is too old (5+ minutes)
        $ArgumentsHash = md5(serialize($Arguments)); // Each Arguments product different output
        if(
            !isset($this->Cache[$NotCachedMethod])
            or !isset($this->Cache[$NotCachedMethod][$ArgumentsHash])
            or ((time() - $this->Cache[$NotCachedMethod][$ArgumentsHash]['Updated']) > (5 * 60))
        ){
            // Rebuild the Cached Result
            $NotCachedResult = call_user_func_array(array($this, $NotCachedMethod), $Arguments);
            // Store the Cache again
            $this->Cache[$NotCachedMethod][$ArgumentsHash] = array(
                'Method'    => $NotCachedMethod,
                'Result'    => $NotCachedResult,
                'Updated'   => time(),
            );
        }
        // Deliver the Cached result
        return $this->Cache[$NotCachedMethod][$ArgumentsHash]['Result'];
    }
}
$Cache = new Cachable();
var_dump($Cache->Method1());
var_dump($Cache->Method1Cached()); // or $Cache->Method1_Cached()
sleep(5);
var_dump($Cache->Method1());
var_dump($Cache->Method1Cached()); // or $Cache->Method1_Cached()

这是使用内部存储使用的,但您可以使用DB来创建自己的瞬态存储。只需将_CachedCached附加到任何存在的方法即可。显然,你可以改变寿命甚至更多。

这只是概念的证明。还有很大的改进空间:)

以下是一篇关于php 中缓存主题的文章的摘录

/**
 * Caching aspect
 */
class CachingAspect implements Aspect
{
   private $cache = null;
   public function __construct(Memcache $cache)
   {
      $this->cache = $cache;
   } 
/**
 * This advice intercepts the execution of cacheable methods
 *
 * The logic is pretty simple: we look for the value in the cache and if we have a cache miss
 * we then invoke original method and store its result in the cache.
 *
 * @param MethodInvocation $invocation Invocation
 *
 * @Around("@annotation(Annotation'Cacheable)")
 */
public function aroundCacheable(MethodInvocation $invocation)
{
    $obj   = $invocation->getThis();
    $class = is_object($obj) ? get_class($obj) : $obj;
    $key   = $class . ':' . $invocation->getMethod()->name;
    $result = $this->cache->get($key);
    if ($result === false) {
        $result = $invocation->proceed();
        $this->cache->set($key, $result);
    }
    return $result;
   }
}

对我来说更有意义,因为它以一种坚实的实现方式交付。我不太喜欢用注释实现同样的功能,我更喜欢更简单的东西。