Использование __get () (магия) для эмуляции корректности для чтения и ленивой загрузки

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

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


Изменить: Тесты скорости

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

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

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

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

введите описание изображения здесь

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

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

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

Результаты тестов

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

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

Что касается Uneeded Added Complexity, вы пишете

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

Конечно. Вы можете легко обнаружить это, посмотрев глубину вложенности вашего кода. Хороший код остается слева. Ваш 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.

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

И это останется на 2 за каждый дополнительный простой старый Getter, который вы добавите.

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

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

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

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

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

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

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

Качество кода: WTF за минуту

Чтобы подойти к концу:

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

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

Дополнительные подходы к Lazy Loading см. В разных моделях Lazy Loading от PoEAA от Мартина Фаулера :

Существует четыре основных разновидности ленивой нагрузки. Lazy Initialization использует специальное значение маркера (обычно null), чтобы указать, что поле не загружено. Каждый доступ к полю проверяет поле для значения маркера и, если он выгружен, загружает его. Виртуальный прокси – это объект с тем же интерфейсом, что и реальный объект. В первый раз один из его методов называется загрузкой реального объекта, а затем делегатов. Value Holder – это объект с методом getValue. Клиенты call getValue получают реальный объект, первый вызов вызывает загрузку. Призрак – это реальный объект без каких-либо данных. При первом вызове метода призрак загружает полные данные в свои поля.

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

Если ваша капитализация имен классов и имен ключей в $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.. } } 

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

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

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

Как волшебные _get так и общие методы getter помогают обеспечить ценность. Однако то, что вы не можете сделать в PHP, это создать свойство только для чтения.

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

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

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

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

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

Использование __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, так как это вряд ли окажет существенное влияние на ваше приложение.