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

Posted

技术标签:

【中文标题】在 PHP 中实现方法结果缓存的装饰器模式的最佳方法【英文标题】:Best way to implement a decorator pattern for method result caching in PHP 【发布时间】:2013-07-03 09:04:23 【问题描述】:

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

应用缓存之前:

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);

不幸的是,我现在要完成手动将其添加到所有方法的稍微费力的任务——我相信这是装饰器模式的一个用例,但我不知道如何以更简单的方式实现它在这种情况下使用 php

最好的方法是什么,希望这些类中的任何一个 所有 方法都能自动执行此操作,或者我只需在方法中添加一行等?

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

谢谢!

【问题讨论】:

【参考方案1】:

这是一篇文章around the subject of caching in 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;
   

对我来说更有意义,因为它以 SOLID 实现方式提供。 我不太喜欢用注释来实现相同的功能,我更喜欢更简单的东西。

【讨论】:

【参考方案2】:

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

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

然后您将需要缓存的实例包装到此装饰器中,如下所示:

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

显然,$cachedInstance 没有 foo() 方法。这里的诀窍是 utilize the magic __call method to intercept all calls to inaccessible or non-existing methods 并将它们委托给装饰实例。通过这种方式,我们通过装饰器公开了装饰实例的整个公共 API。

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

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

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


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

    创建装饰实例公共 API 的接口。您可以手动完成,如果工作量很大,请使用我的Interface Distiller) 将期望装饰实例的每个方法的 TypeHint 更改为接口 让 Decorated 实例实现它。 让装饰器实现它并将任何方法委托给装饰实例 修改所有需要缓存的方法 对所有想要使用装饰器的类重复此操作

简而言之

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
    

缺点是,它需要更多的工作。您需要为每个应该使用缓存的类执行此操作。您还需要将对缓存后端的检查放入每个函数(代码重复),以及将不需要缓存的任何调用委托给装饰实例(繁琐且容易出错)。

【讨论】:

违反了 Liskov 替换原则,看看 Proxy Manager 项目,也许有帮助,帮了我 @decebal 使用__call 的方法很明显,但我明确提到它不是类型安全的,所以这是隐含的。 “经典”装饰器不违反 LSP,因为它实现了相同的接口。【参考方案3】:

使用__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()

这用于内部存储,但您可以为此使用数据库并创建自己的瞬态存储。只需将_CachedCached 附加到任何存在的方法。显然,您可以改变寿命等等。

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

【讨论】:

这有点违反 Liskov 替换原则,但它是一个很好的例子,让我很好奇你将如何实现 apc 和 memcache ?

以上是关于在 PHP 中实现方法结果缓存的装饰器模式的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

如何在 PHP 中实现装饰器?

在 PHP 中构造装饰器

设计模式之装饰器模式

PHP设计模式之装饰器模式

如何在 Java 中实现包装装饰器?

设计模式——装饰器模式