我有一些对我很有用的模式,但是我向其他程序员解释时遇到了一些困难。我正在寻找一些理由或文献参考。
我个人使用PHP,但这也适用于Java,Javascript,C ++和类似语言。例子将是PHP或Pseudocode,我希望你能忍受这个。
我们的想法是使用惰性评估容器来处理中间结果,以避免多次计算相同的中间值。
http://en.wikipedia.org/wiki/Dynamic_programming
动态编程方法只寻求解决每个子问题一次,从而减少计算次数:一旦计算出给定子问题的解,就存储或“备忘”:下次同样的解决方案是需要,只需查阅
class LazyEvaluationContainer {
protected $values = array();
function get($key) {
if (isset($this->values[$key])) {
return $this->values[$key];
}
if (method_exists($this, $key)) {
return $this->values[$key] = $this->$key();
}
throw new Exception("Key $key not supported.");
}
protected function foo() {
// Make sure that bar() runs only once.
return $this->get('bar') + $this->get('bar');
}
protected function bar() {
.. // expensive computation.
}
}
使用类似的容器,例如作为依赖注入容器(DIC)。
我通常会使用其中的一些变体。
在Spring @Configuration类中讨论一个可爱的机制已经有了一个很好的答案。
为了使这个更有用和有趣,我稍微扩展/澄清了一下这个问题:
答案 0 :(得分:1)
如果我理解正确,这是一个非常标准的程序,但正如你正确承认的那样,与DI(或自举应用程序)相关联。
具体的规范示例是任何具有懒惰bean定义的Spring @Configuration
类;我认为它显示与您描述的完全相同的行为,尽管实现它的实际代码在视图中隐藏(并在幕后生成)。实际的Java代码可能是这样的:
@Configuration
public class Whatever {
@Bean @Lazy
public OneThing createOneThing() {
return new OneThing();
}
@Bean @Lazy
public SomeOtherThing createSomeOtherThing() {
return new SomeOtherThing();
}
// here the magic begins:
@Bean @Lazy
public SomeThirdThing getSomeThirdThing() {
return new SomeThirdThing(this.createOneThing(), this.createOneThing(), this.createOneThing(), createSomeOtherThing());
}
}
标有@Bean @Lazy
的每个方法代表一个“资源”,一旦需要就会被创建(并调用该方法)并且 - 无论方法被调用多少次 - 对象都将只创建一次(由于某些魔法会在加载过程中改变实际代码)。因此,即使createOneThing()
中createOneThing()
被调用两次createSomeThirdThing()
,也只会发生一次通话(仅在有人试图呼叫getBean(SomeThirdThing.class)
或致电ApplicationContext
之后{{1}})。
答案 1 :(得分:1)
我认为你无法拥有一个通用的懒惰评估容器。
让我们先讨论一下你到底有什么。我不认为这是懒惰的评价。延迟评估被定义为将评估延迟到真正需要该值的点,并将进一步请求该值分享已评估的值。
我想到的典型例子是数据库连接。您需要准备好能够在需要时使用该连接的所有内容,但只有在确实需要数据库查询时,才会创建连接,然后与后续查询共享。
典型的实现是将连接字符串传递给构造函数,在内部存储它,当调用查询方法时,首先调用返回连接句柄的方法,这将创建并保存该句柄如果它不存在,则使用连接字符串。稍后对该对象的调用将重用现有连接。
这样的数据库对象有资格延迟评估数据库连接:它只在真正需要时创建,然后为每个其他查询共享。
当我查看您的实现时,它不符合"只有在真正需要"时进行评估,它才会存储曾经创建的值。所以它真的只是某种缓存。
它也没有真正解决普遍仅在全球范围内评估昂贵计算的问题。如果您有两个实例,则将运行两次昂贵的功能。但另一方面,不进行两次评估会引入全局状态 - 除非明确声明,否则应视为坏事。通常它会使代码很难正确测试。就个人而言,我避免这样做。
可以将实际数据方法放在与数据计算方法不同的对象中吗?
如果您看一下Zend Framework如何提供缓存模式(\Zend\Cache\Pattern\{Callback,Class,Object}Cache
),您会发现真正的工作类正在使用装饰器。获取值并将其读回的所有内部内容都在内部处理,从外部您就像以前一样调用您的方法。
缺点是您没有原始类型的对象。因此,如果使用类型提示,则无法传递装饰的缓存对象而不是原始对象。解决方案是实现一个接口。原始类使用实际函数实现它,然后创建另一个扩展缓存装饰器的类并实现接口。此对象将传递类型提示检查,但您必须手动实现所有接口方法,这些方法只会将调用传递给内部魔术函数,否则会拦截它们。
interface Foo
{
public function foo();
}
class FooExpensive implements Foo
{
public function foo()
{
sleep(100);
return "bar";
}
}
class FooCached extends \Zend\Cache\Pattern\ObjectPattern implements Foo
{
public function foo()
{
//internally uses instance of FooExpensive to calculate once
$args = func_get_args();
return $this->call(__FUNCTION__, $args);
}
}
我发现PHP在实现缓存时至少没有这两个类和一个接口是不可能的(但另一方面,针对接口实现是一件好事,它不应该打扰你)。您不能直接使用本机缓存对象。
使用带嵌套数组的缓存可以使用带参数的计算方法吗?
参数在上述实现中有效,它们用于内部生成缓存键。您应该查看\Zend\Cache\Pattern\CallbackCache::generateCallbackKey
方法。
在PHP中,可以使用魔术方法(__get()或__call())作为主要检索方法。结合" @ property"在类docblock中,这允许每个"虚拟"的类型提示。属性。强>
魔法是邪恶的。文档块应该被认为是过时的,因为它不是真正的工作代码。虽然我发现在一个非常容易理解的值对象代码中使用magic getter和setter是可以接受的,这样就可以在任何属性中存储任何值,就像{ {1}},我建议您对stdClass
非常小心。
我经常使用" get_someValue()"等方法名称,其中" someValue"是实际的关键,区别于常规方法。
我认为这违反了PSR-1:" 4.3。方法:方法名称必须在__call
中声明。"是否有理由将这些方法标记为特殊的东西?他们是特别的吗?确实返回了价值,不是吗?
可以将数据计算分配给多个对象,以获得某种关注点分离吗?
如果缓存一个复杂的对象结构,这是完全可能的。
可以预先初始化一些值吗?
这不应该是缓存的关注,而是实现本身。 NOT执行昂贵的计算但返回预设值有什么意义?如果这是一个真实的用例(如果参数超出定义的范围,则立即返回NULL),它必须是实现本身的一部分。在这种情况下,您不应该依赖对象周围的附加层来返回值。
将动态编程的中间值存储为合法用例吗?
你有动态编程问题吗?您链接的维基百科页面上有这句话:
为了使动态编程适用,问题必须具备两个关键属性:最佳子结构和重叠子问题。如果通过将最优解决方案与非重叠子问题相结合可以解决问题,则可以将策略称为“分而治之”"代替。
我认为现有的模式似乎可以解决您的示例中的懒惰评估部分:Singleton,ServiceLocator,Factory。 (我不是在这里宣传单身人士!)
还有" promises"的概念:如果被问到,则返回承诺返回实际值的对象,但只要现在不需要该值,就会充当可以传递的值替换。您可能希望阅读此博文:http://blog.ircmaxell.com/2013/01/promise-for-clean-code.html
在PHP中实现此功能的最佳做法是什么?有些东西在"细节"又坏又丑?
您使用了一个可能接近Fibonacci示例的示例。我不喜欢这个例子的方面是您使用单个实例来收集所有值。在某种程度上,你在这里聚合全球状态 - 这可能是整个概念的内容。但是全球国家是邪恶的,我不喜欢那个额外的层。而且你还没有真正解决参数问题。
我想知道为什么在camelCase()
内有两次bar()
来电?更明显的方法是直接在foo()
中复制结果,然后"添加"它
总而言之,直到现在我都没有留下太深刻的印象。在这个简单的层面上,我无法预见到这种通用解决方案的真实用例。我喜欢IDE自动建议支持,我不喜欢duck-typing(传递一个只模拟兼容的对象,但无法确保实例)。