0

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

我个人使用 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())作为主要检索方法。结合类文档块中的“@property”,这允许每个“虚拟”属性的类型提示。
  • 我经常使用诸如“get_someValue()”之类的方法名称,其中“someValue”是实际的键,以区别于常规方法。
  • 是否可以将数据计算分配给多个对象,以获得某种关注点分离?
  • 可以预初始化一些值吗?

编辑:问题

关于 Spring @Configuration 类中的一个可爱的机制,已经有一个很好的答案。

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

  • 存储动态编程的中间值是一个合法的用例吗?
  • 在 PHP 中实现这一点的最佳实践是什么?“细节”里的一些东西是不是又丑又丑?
4

2 回答 2

1

我认为你不能对所有东西都有一个通用的惰性评估容器。

让我们首先讨论一下你真正拥有的东西。我不认为这是懒惰的评价。延迟评估被定义为将评估延迟到真正需要该值的点,并将已评估的值与对该值的进一步请求共享。

我想到的典型例子是数据库连接。您将准备好一切,以便在需要时能够使用该连接,但只有在确实需要数据库查询时,才会创建连接,然后与后续查询共享。

典型的实现是将连接字符串传递给构造函数,将其存储在内部,当调用查询方法时,首先调用返回连接句柄的方法,该方法将创建并保存该连接句柄如果不存在则为字符串。以后对该对象的调用将重用现有的连接。

这样的数据库对象将有资格对数据库连接进行延迟评估:它仅在真正需要时创建,然后为所有其他查询共享。

当我查看您的实现时,它不符合“仅在真正需要时评估”的条件,它只会存储曾经创建的值。所以它实际上只是某种缓存。

它也没有真正解决普遍只在全局范围内评估昂贵计算的问题。如果您有两个实例,您将运行两次昂贵的函数。但另一方面,不对其进行两次评估会引入全局状态——除非明确声明,否则这应该被认为是一件坏事。通常它会使代码很难正确测试。我个人会避免这种情况。

可以将实际数据方法与数据计算方法放在不同的对象中吗?

如果您查看 Zend 框架如何提供缓存模式 ( \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”,这允许每个“虚拟”属性的类型提示。

魔法方法是邪恶的。文档块应该被认为是过时的,因为它不是真正的工作代码。虽然我发现在一个非常容易理解的值对象代码中使用魔法 getter 和 setter 是可以接受的,这将允许在任何属性中存储任何值,就像.stdClass__call

我经常使用诸如“get_someValue()”之类的方法名称,其中“someValue”是实际的键,以区别于常规方法。

我认为这违反了 PSR-1:“4.3. 方法:方法名称必须在 . 中声明camelCase()。” 是否有理由将这些方法标记为特殊的?它们很特别吗?确实返回值,不是吗?

是否可以将数据计算分配给多个对象,以获得某种关注点分离?

如果您缓存对象的复杂构造,这是完全可能的。

可以预初始化一些值吗?

这不应该是缓存的问题,而是实现本身的问题。不执行昂贵的计算,而是返回预设值有什么意义?如果这是一个真实的用例(例如,如果参数超出定义的范围,则立即返回 NULL),它必须是实现本身的一部分。在这种情况下,您不应依赖对象周围的附加层来返回值。

存储动态编程的中间值是一个合法的用例吗?

你有动态规划问题吗?您链接的维基百科页面上有这句话:

为了使动态规划适用,问题必须具有两个关键属性:最优子结构和重叠子问题。如果可以通过组合非重叠子问题的最优解来解决问题,则该策略称为“分而治之”。

我认为已经有一些现有的模式似乎可以解决您示例中的惰性评估部分:Singleton、ServiceLocator、Factory。(我不是在这里推广单身人士!)

还有“承诺”的概念:返回的对象承诺稍后如果被要求返回实际值,但只要现在不需要该值,就会充当可以传递的值替换。您可能想阅读这篇博文:http: //blog.ircmaxell.com/2013/01/promise-for-clean-code.html

在 PHP 中实现这一点的最佳实践是什么?“细节”里的一些东西是不是又丑又丑?

您使用了一个可能接近斐波那契示例的示例。我不喜欢该示例的方面是您使用单个实例来收集所有值。在某种程度上,你在这里聚合了全局状态——这可能就是整个概念的意义所在。但是全局状态是邪恶的,我不喜欢那个额外的层。而且您还没有真正解决足够的参数问题。

bar()我想知道为什么里面真的有两个电话foo()?更明显的方法是将结果直接复制到 中foo(),然后“添加”它。

总而言之,直到现在我都没有印象深刻。在这个简单的层面上,我无法预料到这样一个通用解决方案的真实用例。我确实喜欢 IDE 自动建议支持,但我不喜欢鸭式打字(传递一个仅模拟兼容但无法确保实例的对象)。

于 2013-11-06T22:24:11.283 回答
1

如果我对您的理解正确,这是一个相当标准的过程,尽管正如您正确承认的那样,与 DI(或引导应用程序)相关联。

一个具体的、规范的示例是任何@Configuration具有惰性 bean 定义的 Spring 类;我认为它显示的行为与您描述的完全相同,尽管完成它的实际代码隐藏在视图之外(并在幕后生成)。实际的 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有在加载过程中改变实际代码的魔法)。因此,即使看起来 increateOneThing()被调用了两次 in createOneThing(),也只会发生一次调用(而且那只是在有人尝试调用createSomeThirdThing()或调用getBean(SomeThirdThing.class)on之后ApplicationContext)。

于 2013-10-30T23:29:23.570 回答