【问题标题】:Best way to implement a decorator pattern for method result caching in PHP在 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 语句等的方法,但实际上什么都看不到。

谢谢!

【问题讨论】:

    标签: php oop design-patterns caching decorator


    【解决方案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 类型的实例时,您将收到错误。


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

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

      简而言之

      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 ?
      猜你喜欢
      • 1970-01-01
      • 2013-05-04
      • 2011-09-17
      • 2019-12-10
      • 2010-11-27
      • 1970-01-01
      • 2013-08-17
      • 2013-02-25
      相关资源
      最近更新 更多