【问题标题】:Using __get() (magic) to emulate readonly properites and lazy-loading使用 __get()(魔术)模拟只读属性和延迟加载
【发布时间】:2012-02-18 15:41:56
【问题描述】:

我正在使用__get() 使我的一些属性“动态”(仅在请求时初始化它们)。这些“假”属性存储在私有数组属性中,我正在 __get 中检查。

无论如何,您认为为每个属性创建方法而不是在 switch 语句中创建方法更好吗?


编辑:速度测试

我只关心性能,@Gordon 提到的其他东西对我来说并不重要:

  • 不必要的额外复杂性 - 它并没有真正增加我的应用复杂性
  • 脆弱的非显而易见的 API - 我特别希望我的 API 被“隔离”;文档应该告诉其他人如何使用它:P

所以这是我所做的测试,这让我认为性能命中参数是不合理的:

50.000 次调用的结果(在 PHP 5.3.9 上):

(t1 = 带开关的魔法,t2 = getter,t3 = 带进一步 getter 调用的魔法)

不确定 t3 上的“Cum”是什么意思。它不能是累积时间,因为 t2 应该有 2K 那么...

代码:

class B{}



class A{
  protected
    $props = array(
      'test_obj' => false,
    );

  // magic
  function __get($name){
    if(isset($this->props[$name])){
      switch($name){

        case 'test_obj':
          if(!($this->props[$name] instanceof B))
            $this->props[$name] = new B;

        break;
      }

      return $this->props[$name];
    }

    trigger_error('property doesnt exist');
  }

  // standard getter
  public function getTestObj(){
    if(!($this->props['test_obj'] instanceof B))
      $this->props['test_obj'] = new B;

    return $this->props['test_obj'];
  }
}



class AA extends A{

  // magic
  function __get($name){
    $getter = "get".str_replace('_', '', $name); // give me a break, its just a test :P

    if(method_exists($this, $getter))
      return $this->$getter();


    trigger_error('property doesnt exist');
  }


}


function t1(){
  $obj = new A;

  for($i=1;$i<50000;$i++){
    $a = $obj->test_obj;

  }
  echo 'done.';
}

function t2(){
  $obj = new A;

  for($i=1;$i<50000;$i++){
    $a = $obj->getTestObj();

  }
  echo 'done.';
}

function t3(){
  $obj = new AA;

  for($i=1;$i<50000;$i++){
    $a = $obj->test_obj;

  }
  echo 'done.';
}

t1();
t2();
t3();

ps:为什么我要使用 __get() 而不是标准的 getter 方法?唯一的原因是api美观;因为我没有看到任何真正的缺点,我想这是值得的:P


编辑:更多速度测试

这次我用 microtime 来测量一些平均值:

PHP 5.2.4 和 5.3.0(结果相似):

t1 - 0.12s
t2 - 0.08s
t3 - 0.24s

PHP 5.3.9,xdebug 处于活动状态,这就是它如此缓慢的原因:

t1 - 1.34s
t2 - 1.26s
t3-  5.06s

禁用 xdebug 的 PHP 5.3.9:

t1 - 0.30
t2 - 0.25
t3 - 0.86


另一种方法:
 // magic
  function __get($name){
    $getter = "get".str_replace('_', '', $name);

    if(method_exists($this, $getter)){
      $this->$name = $this->$getter();   // <-- create it
      return $this->$name;                
    }


    trigger_error('property doesnt exist');
  }

在第一次 __get 调用之后,将动态创建具有请求名称的公共属性。这解决了速度问题 - 在 PHP 5.3 中获得 0.1 秒(它比标准 getter 快 12 倍),以及 Gordon 提出的可扩展性问题。您可以简单地覆盖子类中的 getter。

缺点是属性变得可写:(

【问题讨论】:

  • 在这些测试中,“cum”是函数花费的时间,包括从内部调用的任何函数的时间(基本上是从函数开始执行到它返回)。这与“self”时间不同,后者在调用其他函数时有效地“暂停”计时器。在这种情况下,t3 的 "cum" 减去 "self" 计算出在 $this->$getter() 调用中花费了多少时间,而 "self" 是它执行其他操作所花费的时间__get() 函数。

标签: php performance oop


【解决方案1】:

这是 Zend Debugger 在我的 Win7 机器上使用 PHP 5.3.6 报告的代码结果:

如您所见,对__get 方法的调用比常规调用慢很多(3-4 倍)。总共 50k 调用我们仍然处理不到 1 秒,因此在小规模使用时可以忽略不计。但是,如果您的意图是围绕魔术方法构建整个代码,您将需要分析最终应用程序,看看它是否仍然可以忽略不计。

对于相当无趣的性能方面来说就这么多。现在让我们看看您认为“不那么重要”的内容。我要强调这一点,因为它实际上比性能方面更重要。

关于你编写的不需要的额外复杂性

它并没有真正增加我的应用程序复杂性

当然可以。您可以通过查看代码的嵌套深度轻松发现它。好的代码留在左边。你的 if/switch/case/if 有四个层次。这意味着有更多可能的执行路径,这将导致更高的Cyclomatic Complexity,这意味着更难维护和理解。

这是 A 类的数字(没有常规的 Getter。输出从 PHPLoc 缩短):

Lines of Code (LOC):                                 19
  Cyclomatic Complexity / Lines of Code:           0.16
  Average Method Length (NCLOC):                     18
  Cyclomatic Complexity / Number of Methods:       4.00

值 4.00 表示这已经处于中等复杂性的边缘。您在交换机中每增加一个外壳,这个数字就会增加 2。此外,它会将您的代码变成程序混乱,因为所有逻辑都在 switch/case 内部,而不是将其划分为离散单元,例如单吸气剂。

Getter,即使是延迟加载的,也不需要适度复杂。考虑使用普通的旧 PHP Getter 的同一个类:

class Foo
{
    protected $bar;
    public function getBar()
    {
        // Lazy Initialization
        if ($this->bar === null) {
            $this->bar = new Bar;
        }
        return $this->bar;
    }
}

在这上面运行 PHPLoc 会给你一个更好的循环复杂度

Lines of Code (LOC):                                 11
  Cyclomatic Complexity / Lines of Code:           0.09
  Cyclomatic Complexity / Number of Methods:       2.00

对于您添加的每一个额外的普通旧 Getter,这将保持为 2。

另外,请注意,当您想使用变体的子类型时,您必须重载 __get 并复制并粘贴整个 switch/case 块以进行更改,而使用普通的旧 Getter 则只需重载您需要更改的 Getter。

是的,添加所有 Getter 需要更多的输入工作,但它也更简单,最终将导致更可维护的代码,并且还具有为您提供显式 API 的好处,这将我们引向您的其他声明

我特别希望我的 API 被“隔离”;文档应该告诉其他人如何使用它:P

我不知道你所说的“孤立”是什么意思,但如果你的 API 不能表达它的作用,那么它就是糟糕的代码。如果我必须阅读您的文档,因为您的 API 没有告诉我如何通过查看它来与它交互,那么您做错了。您正在混淆代码。在数组中声明属性而不是在类级别(它们所属的位置)声明它们会迫使您为其编写文档,这是额外且多余的工作。好的代码易于阅读和自我记录。考虑购买 Robert Martin 的《清洁代码》一书。

话虽如此,当你说

唯一的原因是api美观;

然后我说:那就不要使用__get,因为它会产生相反的效果。它会使 API 变得丑陋。魔术是复杂且不明显的,而这正是导致这些 WTF 时刻的原因:

现在结束:

我没有看到任何真正的缺点,我想这是值得的

希望你现在能看到它们。不值得。

有关延迟加载的其他方法,请参阅various Lazy Loading patterns from Martin Fowler's PoEAA

有四种主要类型的延迟加载。 延迟初始化使用特殊的标记值(通常为 null)来指示未加载字段。每次访问该字段都会检查该字段的标记值,如果未加载,则加载它。 Virtual Proxy是与真实对象具有相同接口的对象。第一次调用它的一个方法时,它会加载真实的对象,然后进行委托。 Value Holder 是一个带有 getValue 方法的对象。客户端调用 getValue 获取真实对象,第一次调用触发加载。 ghost 是没有任何数据的真实对象。第一次调用方法时,ghost 会将完整数据加载到其字段中。

这些方法略有不同,并且有各种权衡。您还可以使用组合方法。本书包含完整的讨论和示例。

【讨论】:

  • 我必须承认你对这两点是对的,但关于速度我认为 Zend Debugger 是在撒谎!
  • 您如何看待在第一次 __get 调用时创建属性? :)
  • @Alex 我不喜欢它。如果我需要知道该类定义了哪些属性,我将不得不从方法体中收集它,而不是仅仅查看类声明的顶部。此外,当您动态声明它们时,这些属性将是公开的。如果你想要这样的东西,你可能会喜欢 Python :)
  • @Gordon 我完全同意你的帖子。可读性、可维护性和较低的复杂性将带来一个好的 api 和使用它的更好的软件。 +1。顺便说一句,亚历克斯,想想 80:20 规则。 80% 的 cpu 容量将花费在 20% 的代码中(即算法、等待用户输入等)。如果您使用 __get 或普通的旧 getter,通常对性能无关紧要
  • @contrebis 是的,确实如此。 I have answered how to document magic methods 过去。虽然它确实有帮助,但您仍然必须依赖文档而不是依赖代码本身。我看不出这样做有什么好处。
【解决方案2】:

如果你的类名和$prop 中的键名的大小写匹配,你可以这样做:

class Dummy {
    private $props = array(
        'Someobject' => false,
        //etc...
    );

    function __get($name){
        if(isset($this->props[$name])){
            if(!($this->props[$name] instanceof $name)) {
                $this->props[$name] = new $name();
            }

            return $this->props[$name];
        }

        //trigger_error('property doesnt exist');
        //Make exceptions, not war
        throw new Exception('Property doesn\'t exist');     
    }
}

即使大小写不匹配,只要它遵循相同的模式,它也可以工作。如果第一个字母总是大写,您可以使用ucfirst() 来获取类名。


编辑

使用简单的方法可能会更好。在 getter 内部有一个开关,特别是当您尝试获取的每件事情执行的代码不同时,实际上违背了 getter 的目的,使您不必重复代码。采取简单的方法:

class Something {
    private $props = array('Someobject' => false);

    public getSomeobject() {
        if(!($this->props['Someobject'] instanceof Someobject)) {
            //Instantiate and do extra stuff
        }

        return $this->props['Someobject'];
    }

    public getSomeOtherObject() {
        //etc..
    }
}

【讨论】:

  • 它不遵循相同的模式。在某些陈述中,我需要做的不仅仅是实例化类......
  • @Alex 最好只使用普通方法。
【解决方案3】:

我正在使用 __get() 使我的一些属性“动态”(仅在请求时初始化它们)。这些“假”属性存储在私有数组属性中,我正在 __get 中检查。

无论如何,您认为为每个属性创建方法而不是在 switch 语句中创建方法更好吗?

您提出问题的方式我认为这实际上与任何人的想法无关。谈思想,首先要明确这里要解决什么问题。

神奇的_get 和普通的getter methods 都有助于提供价值。但是,在 PHP 中你不能做的是创建一个只读属性。

如果你需要一个只读属性,你只能使用 PHP 中的神奇 _get 函数来做到这一点(替代方法是在 RFC 中)。

如果您对访问器方法没问题,并且担心键入方法的代码,如果您真的担心编写方面的问题,请使用更好的 IDE。

如果这些属性不需要具体,您可以让它们保持动态,因为更具体的接口将是一个无用的细节,只会使您的代码比它需要的更复杂,因此违反了常见的 OO 设计原则。

但是,动态或魔法也可能表明您做错了什么。而且也很难调试。所以你真的应该知道你在做什么。这需要您使您想解决的问题更加具体,因为这在很大程度上取决于对象的类型。

速度是你不应该单独测试的东西,它不会给你很好的建议。你的问题的速度听起来更像是一种药物;)但服用这种药物不会让你有能力做出明智的决定。

【讨论】:

    【解决方案4】:

    据说使用__get() 会影响性能。因此,如果您的参数列表是静态的/固定的并且不是很长,那么为每个参数创建方法并跳过__get() 在性能方面会更好。例如:

    public function someobject() {
        if(!($this->props[$name] instanceof Someobject))
            $this->props[$name] = new Someobject;
            // do stuff to initialize someobject
        }
        if (count($argv = func_get_args())) {
            // do stuff to SET someobject from $a[0]
        }
        return $this->props['someobject'];
    }
    

    为了避免魔法方法,你必须像这样改变你使用它的方式

    $bar = $foo->someobject; // this won't work without __get()
    $bar = $foo->someobject(); // use this instead
    
    $foo->someobject($bar); // this is how you would set without __set()
    

    编辑

    编辑,正如 Alex 指出的那样,性能损失很小。您可以尝试两种方法并进行一些基准测试,或者直接使用 __get,因为它不太可能对您的应用程序产生重大影响。

    【讨论】:

    • 这就是问题.. 我没有看到性能受到影响。使用 xdebug 和 wincache 研磨,我看到 __get 调用的最长时间是 2ms,其中大多数有 0.1ms
    • 是的,无论如何,这就是网络应用程序的问题,大多数性能影响都可以忽略不计,因此除非您是 facebook,否则真的不是问题。也就是说,无论如何我在我的项目中使用 __get。
    • 更奇怪的是我做了一个像GetSomeObject()这样的普通方法,调用它的时间和__get版本一样——0.2ms
    • @Alex 只有在多次调用魔术方法后,性能才会明显。但这不仅会影响性能,还会增加不必要的复杂性和脆弱的、不明显的 API(和行为),再加上 __get 不是 getter 替代品而是错误处理程序这一事实,这与它的广泛使用背道而驰。
    • 哇,这些数字很有说服力,与我之前的经验相反。也许PHP对它们进行了更好的优化,也许我只是记错了。这让我对我在活动记录类中广泛使用魔术方法感觉更好。
    猜你喜欢
    • 1970-01-01
    • 2010-11-19
    • 2012-04-05
    • 2011-06-10
    • 1970-01-01
    • 2015-07-23
    • 2013-08-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多