Использование __get () (magic) для эмуляции свойств readonly и отложенной загрузки - PullRequest
13 голосов
/ 18 февраля 2012

Я использую __ get () , чтобы сделать некоторые из моих свойств "динамическими" (инициализировать их только по запросу).Эти «поддельные» свойства хранятся в частном свойстве массива, которое я проверяю внутри __get.

В любом случае, как вы думаете, лучше будет создать методы для каждого из этих свойств, а не делать это в переключателезаявление?


Редактировать: Тесты скорости

Я беспокоюсь только о производительности, другие вещи, которые упомянул @Гордон, не так важны для меня:

  • ненужная дополнительная сложность - это на самом деле не увеличивает сложность моего приложения
  • хрупкий неочевидный API - я специально хочу, чтобы мой API был «изолированным»;Документация должна рассказать другим, как его использовать: P

Итак, вот мои тесты, которые заставляют меня думать, что показатель производительности неоправдан:

Результаты для 50.000вызовы (в PHP 5.3.9):

enter image description here

(t1 = магия с переключателем, t2 = геттер, t3 = магия с дальнейшим вызовом геттера)

Неконечно, что означает "Cum" на t3.Это не может быть кумулятивным временем, потому что 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 () вместо стандартных методов получения?единственная причина - красота API;потому что я не вижу никаких реальных недостатков, я думаю, это того стоит: P


Редактировать: Больше тестов скорости

На этот раз я использовал микротайм для измерения некоторых средних:

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

PHP5.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.Это решает проблемы со скоростью - получение 0,1 с в PHP 5.3 (это в 12 раз быстрее, чем со стандартным геттером) и проблема расширяемости, поднятая Гордоном.Вы можете просто переопределить метод получения в дочернем классе.

Недостатком является то, что свойство становится доступным для записи: (

Ответы [ 4 ]

18 голосов
/ 19 февраля 2012

Вот результаты вашего кода, представленные Zend Debugger с PHP 5.3.6 на моей машине с Win7:

Benchmark results

Как видите, вызовы к вашим __get методам намного (в 3-4 раза) медленнее, чем обычные вызовы. Мы по-прежнему имеем дело с менее чем 1 с для вызовов в общей сложности 50 000, так что это незначительно при использовании в небольших масштабах. Однако, если вы хотите построить весь код на основе магических методов, вам нужно профилировать конечное приложение, чтобы увидеть, все ли оно незначительно.

Так много для довольно неинтересного аспекта производительности. Теперь давайте посмотрим на то, что вы считаете «не так важно». Я подчеркиваю это, потому что на самом деле это гораздо важнее, чем аспект производительности.

Относительно ненужной добавленной сложности, которую вы пишете

это действительно не увеличивает сложность моего приложения

Конечно, это так. Вы можете легко определить это, посмотрев на глубину вложения вашего кода. Хороший код остается слева. Ваш if / switch / case / if имеет четыре уровня. Это означает, что существует больше возможных путей выполнения, и это приведет к более высокой цикломатической сложности , что означает, что труднее поддерживать и понимать.

Вот числа для вашего класса A (без обычного геттера. Вывод сокращен с 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 для каждого дополнительного футляра, который вы кладете в коммутатор. Кроме того, он превратит ваш код в процедурный беспорядок, потому что вся логика находится внутри переключателя / корпуса, а не делит его на отдельные блоки, например, одиночные добытчики.

Геттер, даже ленивый, не должен быть в меру сложным. Рассмотрим тот же класс с простым старым 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

И это останется равным 2 для каждого дополнительного простого старого получателя, которого вы добавляете.

Кроме того, примите во внимание, что когда вы хотите использовать подтипы вашего варианта, вам придется перегружать __get и копировать и вставлять весь блок switch / case для внесения изменений, тогда как с простым старым Getter вы просто перегружаете Геттеры нужно поменять.

Да, добавление всех геттеров - это более трудоемкая работа, но это также намного проще и, в конечном итоге, приведет к более удобному для сопровождения коду, а также имеет преимущество в предоставлении вам явного API, что приводит нас к вашему другому утверждению

Я специально хочу, чтобы мой API был "изолированным"; Документация должна рассказать другим, как его использовать: P

Я не знаю, что вы подразумеваете под "изолированным", но если ваш API не может выразить то, что он делает, это плохой код. Если мне нужно прочитать вашу документацию, потому что ваш API не говорит мне, как я могу взаимодействовать с ней, глядя на нее, вы делаете это неправильно. Вы запутываете код. Объявление свойств в массиве вместо объявления их на уровне класса (где они принадлежат) заставляет вас писать для него документацию, что является дополнительной и излишней работой. Хороший код легко читается и самодокументируется. Подумайте о покупке книги Роберта Мартина «Чистый код».

С учетом сказанного, когда вы говорите

единственная причина - красота API;

тогда я говорю: тогда не используйте __get, потому что это будет иметь обратный эффект. Это сделает API уродливым. Магия сложна и неочевидна, и это именно то, что приводит к этим моментам WTF:

Code Quality: WTF per minute

Чтобы закончить сейчас:

Я не вижу никаких реальных недостатков, я думаю, это того стоит

Вы, надеюсь, увидите их сейчас. Это не стоит того.

Дополнительные подходы к отложенной загрузке см. В различных шаблонах отложенной загрузки из PoEAA Мартина Фаулера :

Существует четыре основных типа ленивых нагрузок. Ленивая инициализация использует специальное значение маркера (обычно нулевое), чтобы указать, что поле не загружено.Каждый доступ к полю проверяет поле на наличие значения маркера и, если он не загружен, загружает его. Виртуальный прокси - это объект с тем же интерфейсом, что и у реального объекта.При первом вызове одного из его методов он загружает реальный объект, а затем делегирует. Держатель значения - это объект с методом getValue.Клиенты вызывают getValue для получения реального объекта, первый вызов запускает загрузку.A ghost - это реальный объект без каких-либо данных.При первом вызове метода призрак загружает полные данные в свои поля.

Эти подходы несколько различаются и имеют различные компромиссы.Вы также можете использовать комбинированные подходы.Книга содержит полное обсуждение и примеры.

2 голосов
/ 28 июля 2012

Я использую __get (), чтобы сделать некоторые из моих свойств «динамическими» (инициализировать их только по запросу). Эти «поддельные» свойства хранятся в частном свойстве массива, которое я проверяю внутри __get.

В любом случае, как вы думаете, лучше создать методы для каждого из этих свойств вместо того, чтобы делать это в операторе switch?

То, как вы задаете свой вопрос, я не думаю, что это на самом деле о том, что кто-то думает. Чтобы говорить о мыслях, прежде всего должно быть понятно, какую проблему вы хотите решить здесь.

И магические _get, и обычные методы получения помогают указать значение. Однако в PHP вы не можете создать свойство только для чтения.

Если вам нужно свойство только для чтения, вы можете сделать это только с помощью волшебной функции _get в PHP (альтернатива в RFC).

Если вы в порядке с методами доступа, и вы заинтересованы в наборе кода методов, используйте лучшую IDE, которая сделает это за вас, если вы действительно обеспокоены этим аспектом написания.

Если эти свойства просто не обязательно должны быть конкретными, вы можете сохранять их динамичными, потому что более конкретный интерфейс будет бесполезной деталью и сделает ваш код более сложным, чем он должен быть, и, следовательно, нарушает общие принципы проектирования ОО.

Однако динамика или магия также могут быть признаком того, что вы делаете что-то не так. А также трудно отлаживать. Таким образом, вы действительно должны знать, что вы делаете. Для этого вам нужно сделать задачу, которую вы хотели бы решить, более конкретной, поскольку это сильно зависит от типа объектов.

А скорость - это то, что вы не должны тестировать изолированно, это не дает вам хороших советов. Скорость в вашем вопросе больше похожа на наркотик;) но принятие этого препарата не даст вам мудрого решения.

2 голосов
/ 18 февраля 2012

Если ваша заглавная буква имен классов и имен ключей в $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(), чтобы получить имя класса.


РЕДАКТИРОВАТЬ

Это, вероятно, простолучше использовать простые методы.Наличие переключателя внутри геттера, особенно когда код, выполняемый для каждой вещи, которую вы пытаетесь получить, отличается, фактически побуждает цель получения, избавляя вас от необходимости повторять код.Возьмите простой подход:

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..
    }
}
1 голос
/ 18 февраля 2012

Использование __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()

EDIT

Редактировать, как указал Алекс, снижение производительности составляет миллисекунду.Вы можете попробовать оба способа и сделать несколько тестов или просто использовать __get, так как это вряд ли окажет существенное влияние на ваше приложение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...