用于动态编程的惰性评估容器?

时间:2013-10-27 17:09:57

标签: php dynamic-programming lazy-evaluation data-containers

我有一些对我很有用的模式,但是我向其他程序员解释时遇到了一些困难。我正在寻找一些理由或文献参考。

我个人使用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)。

详细信息

我通常会使用其中的一些变体。

  • 可以将实际数据方法放在与数据计算方法不同的对象中吗?
  • 使用具有嵌套数组的缓存可以使用带参数的计算方法吗?
  • 在PHP中,可以使用魔术方法(__get()或__call())作为主检索方法。与docblock类中的“@property”结合使用,可以为每个“虚拟”属性提供类型提示。
  • 我经常使用像“get_someValue()”这样的方法名称,其中“someValue”是实际键,以区别于常规方法。
  • 可以将数据计算分配给多个对象,以获得某种关注点分离吗?
  • 可以预先初始化一些值吗?

编辑:问题

在Spring @Configuration类中讨论一个可爱的机制已经有了一个很好的答案。

为了使这个更有用和有趣,我稍微扩展/澄清了一下这个问题:

  • 将动态编程的中间值存储为合法用例吗?
  • 在PHP中实现此功能的最佳做法是什么? “细节”中的一些东西是不是很难看?

2 个答案:

答案 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(传递一个只模拟兼容的对象,但无法确保实例)。