使用__get()(魔术)来模拟readonly属性和延迟加载

Ale*_*lex 13 php oop performance

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

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


编辑:速度测试

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

  • 不需要增加的复杂性 - 它并没有真正增加我的应用复杂性
  • 脆弱的非显而易见的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();
Run Code Online (Sandbox Code Playgroud)

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


编辑:更多速度测试

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

PHP 5.2.4和5.3.0(类似结果):

t1 - 0.12s
t2 - 0.08s
t3 - 0.24s
Run Code Online (Sandbox Code Playgroud)

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

t1 - 1.34s
t2 - 1.26s
t3-  5.06s
Run Code Online (Sandbox Code Playgroud)

禁用xdebug的PHP 5.3.9:

t1 - 0.30
t2 - 0.25
t3 - 0.86
Run Code Online (Sandbox Code Playgroud)


另一种方法:

 // 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');
  }
Run Code Online (Sandbox Code Playgroud)

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

缺点是属性变得可写:(

Gor*_*don 18

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

基准测试结果

正如您所看到的,对__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
Run Code Online (Sandbox Code Playgroud)

值为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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Lines of Code (LOC):                                 11
  Cyclomatic Complexity / Lines of Code:           0.09
  Cyclomatic Complexity / Number of Methods:       2.00
Run Code Online (Sandbox Code Playgroud)

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

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

是的,添加所有Getters的打字工作更多,但它也更简单,最终会带来更易维护的代码,并且还有为您提供明确API的好处,这将引导我们进行您的其他声明

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

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

有了这个说,当你说

唯一的原因是api美;

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

代码质量:每分钟WTF

现在就结束:

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

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

有关延迟加载的其他方法,请参阅Martin Fowler的PoEAA中各种延迟加载模式:

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

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

  • @Alex我不喜欢它.如果我需要知道类定义了哪些属性,我将不得不从方法体系中收集它,而不是仅仅查看类声明.此外,当您在运行中声明属性时,属性将是公共的.你可能喜欢Python,如果你想要这样的东西:) (2认同)
  • @Gordon我完全赞同你的帖子.可读性,可维护性和较低的复杂性将导致良好的api和更好的软件使用它.+1.顺便说一下Alex,想想80:20的规则.80%的cpu容量将花费在20%的代码中(即算法,等待用户输入等).如果使用__get或普通的旧getter,通常对性能无关紧要 (2认同)
  • @contrebis是的,的确如此.[我已经回答了如何记录魔术方法(http://stackoverflow.com/questions/3814733/code-completion-for-private-protected-member-variables-when-using-magic-get/3815198#3815198)在过去.虽然它确实有帮助,但您仍然需要依赖文档而不是依赖代码本身.这样做我看不出任何好处. (2认同)