在 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()
这用于内部存储,但您可以为此使用数据库并创建自己的瞬态存储。只需将_Cached
或Cached
附加到任何存在的方法。显然,您可以改变寿命等等。
这只是概念证明。还有很大的改进空间:)
【讨论】:
这有点违反 Liskov 替换原则,但它是一个很好的例子,让我很好奇你将如何实现 apc 和 memcache ?以上是关于在 PHP 中实现方法结果缓存的装饰器模式的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章