使用__get()(magic)来模拟只读属性和延迟加载

时间:2012-02-18 15:41:56

标签: php performance oop

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

中检查

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


编辑:速度测试

我只关心表现,@戈登提到的其他东西对我来说并不重要:

  • 不需要增加复杂性 - 它并没有真正提高我的应用程序复杂性
  • 脆弱的非显而易见的API - 我特别想让我的API“孤立”;文档应告诉其他人如何使用它:P

所以这里是我做的测试,这让我觉得表现不佳是不合理的:

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

enter image description here

(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

PHP 5.3.9,禁用xdebug:

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。

缺点是该属性变得可写:(

4 个答案:

答案 0 :(得分:18)

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

Benchmark results

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

非常无趣的表现方面。现在让我们来看看你认为“不重要”的东西。我要强调的是,因为它实际上比性能方面更重要。

关于你所写的不需要的复杂性

  

它并没有真正提高我的应用复杂性

当然可以。您可以通过查看代码的嵌套深度轻松发现它。好的代码留在左边。你的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。此外,它会将您的代码变成程序混乱,因为所有逻辑都在交换机/案例中,而不是将其划分为离散单元,例如单个Getters。

一个Getter,即使是一个懒惰的加载器,也不需要中等复杂。考虑使用普通的旧PHP Getter的同一个类:

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

在此上运行PHPLoc将为您提供更好的Cyclomatic Complexity

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

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

另外,考虑到当你想要使用变体的子类型时,你必须重载__get并复制并粘贴整个开关/案例块以进行更改,而使用普通的旧Getter你只需重载您需要更改的Getters。

是的,添加所有Getters的打字工作更多,但它也更简单,最终会带来更易维护的代码,并且还有一个明确的API,可以引导我们进行另一个声明< / p>

  

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

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

据说,当你说

  

唯一的原因是api美;

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

Code Quality: WTF per minute

现在就结束:

  

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

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

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

  

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

     

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

答案 1 :(得分: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..
    }
}

答案 2 :(得分:2)

  

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

中检查      

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

你提出问题的方式我不认为这实际上与任何人的想法有关。谈谈想法,首先必须清楚你想在这里解决哪个问题。

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

如果你需要一个只读属性,那么到目前为止你只能使用PHP中的魔术_get函数(替代方案在RFC中)。

如果您对访问器方法没有问题,并且您担心键入方法的代码,请使用更好的IDE,如果您真的关心该编写方面,那么可以为您执行此操作。

如果这些属性不需要具体,那么你可以保持它们的动态,因为更具体的界面将是一个无用的细节,只会使你的代码比它需要的更复杂,因此违反了常见的面向对象设计原则。 / p>

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

速度是你不应该测试孤立的东西,它不会给你很好的建议。你问题的速度听起来更像药物;)但服用这种药物不会给你明智的决定权。

答案 3 :(得分:1)

使用__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()

修改

编辑,正如亚历克斯指出的那样,性能命中小毫秒。您可以尝试两种方式并执行一些基准测试,或者只使用__get,因为它不太可能对您的应用程序产生重大影响。