Статьи

Малоизвестные «особенности» OO-модели PHP

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

Наследование для интерфейсов и признаков

Начнем с привычной территории: интерфейсы. Интерфейс в PHP позволяет нам определять контракт, который должен реализовать любой объект, реализующий этот интерфейс. Но знаете ли вы, что интерфейсы могут также наследовать другие интерфейсы и что родительский или дочерний интерфейс может быть реализован классом?

Рассмотрим этот код, который определяет интерфейс, другой, расширяющий его, и класс, реализующий дочерний интерфейс:

<?php interface Reversible { function reverse($target); } interface Recursible extends Reversible { function recurse($target); } class Tricks implements Recursible { public function recurse($target) { // something cool happens here } public function reverse($target) { // something backward happens here } } 

Я определил интерфейс с именем Reversible и еще один Recursible именем Recursible , расширяющий его. Когда я реализую Recursible в классе Tricks , должны присутствовать как метод Recursible recurse() из интерфейса Recursible и метод reverse() из интерфейса Reversible .

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

Черты предлагают аналогичную модель. Если у вас еще не было возможности использовать признаки, они выглядят как классы и могут содержать полные определения методов, которые можно применять к любому классу (классам) в вашем приложении, без необходимости наследования из общего расположения. Нас часто учат, что лучше перемещать код в общий родительский класс, чем копировать и вставлять между классами, но иногда классы не связаны, и наследование ложно. Черты — отличная особенность, потому что они позволяют нам повторно использовать код, даже если объекты недостаточно похожи, чтобы оправдать наследование.

Давайте посмотрим на пример простой черты. (Предупреждение: схема именования zany из предыдущего примера все еще находится в действии.)

 <?php trait Reversible { public function reverse($target) { return array_reverse($target); } } 

Синтаксис признаков выглядит очень похоже на класс, и действительно, признаки могут содержать как свойства, так и методы, включая абстрактные методы. Затем они применяются в классе, который вводит в него признак с помощью ключевого слова use … или их также можно применить к признаку, как мы видим здесь:

 <?php trait RecursivelyReversible { use Reversible; public function reverseRecursively($target) { foreach($target as $key => $item) { if(is_array($item)) { $target[$key] = $this->reverseRecursively($item); } } return $this->reverse($target); } } 

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

 <?php class Mirror { use RecursivelyReversible; } $array = [1, "green", "blue", ["cat", "sat", "mat", [0,1,2]]]; $reflect = new Mirror(); print_r($reflect->reverseRecursively($array)); 

Если вы запустите код, то увидите, что не только верхний уровень массива меняется на противоположный, но и PHP также детализирует и инвертирует все дочерние элементы.

Насколько частной является частная собственность?

То есть вы подумали, что частная собственность доступна только из текущего объекта? Не совсем верно! На самом деле ограничение касается только имени класса, поэтому объекты одного класса могут обращаться к закрытым свойствам и методам друг друга. Чтобы проиллюстрировать это, я создал класс с закрытым свойством и открытым методом, который принимает экземпляр того же класса объекта в качестве аргумента:

 <?php class Storage { private $things = []; public function add($item) { $this->things[] = $item; } public function evaluate(Storage $container) { return $container->things; } } $bucket = new Storage(); $bucket->add("phone"); $bucket->add("biscuits"); $bucket->add("handcream"); $basket = new Storage(); print_r($basket->evaluate($bucket)); 

Вы можете подумать, что $basket не будет иметь доступа к частным данным $bucket , но на самом деле приведенный выше код работает просто отлично! Спрашивать, является ли это поведение гочей или особенностью, все равно, что спрашивать, является ли растение цветком или сорняком; это зависит от вашего намерения и перспективы.

Как выглядит абстрактный класс?

Абстрактный класс обычно считается неполным классом; мы определяем только частичную функциональность и используем ключевое слово abstract чтобы остановить попытки создания чего-либо.

 <?php class Incomplete { abstract public function notFinished(); } 

Если вы попытаетесь создать экземпляр Incomplete , вы увидите следующую ошибку:

  Фатальная ошибка PHP: класс Incomplete содержит 1 абстрактный метод
 и поэтому должен быть объявлен абстрактным или реализовать
 остальные методы (Incomplete :: notFinished) в / home / lorna /
 sitepoint / oop-features / incomplete.php в строке 5 

Сообщение не требует пояснений, но теперь рассмотрим другой класс:

 <?php abstract class PerfectlyGood { public function doCoolStuff() { // cool stuff return true; } } 

Это совершенно допустимый класс, кроме abstract ключевого слова. На самом деле, вы можете пометить любой класс как абстрактный, если хотите. Другие классы могут расширять его, но сам по себе он не может быть создан. Это может быть полезным устройством для разработчиков библиотек, которые хотят, чтобы разработчики расширяли свои классы, а не использовали их напрямую. Именно поэтому Zend Framework имеет богатую традицию абстрактных классов.

Тип подсказки не автозагрузка

Мы используем подсказки типов, чтобы гарантировать, что входящий параметр метода удовлетворяет определенным требованиям, давая имя класса или интерфейса, которым он должен быть (или должен быть связан). Однако PHP не вызывает автозагрузчик, если класс или интерфейс, указанный в подсказке типа, еще не объявлен; мы просто увидим ошибку объявления отсутствующего класса.

 <?php namespace MyNamespace; class MyException extends Exception { } class MyClass { public function doSomething() { throw new MyException("you fool!"); } } try { $myclass = new MyClass(); $myclass->doSomething(); echo "that went well"; } catch (Exception $e) { echo "uh oh... " . $e->getMessage(); } 

Имя класса в предложении catch на самом деле является подсказкой типа, но поскольку мы не указали, что класс Exception находится в пространстве имен верхнего уровня, PHP считает, что мы имеем в виду MyNamespaceException которого не существует. Отсутствующий класс не вызывает ошибку, но наше исключение теперь пропускает предложение catch . Вы можете ожидать, что мы увидим отсутствующее сообщение MyNamespaceException , но вместо этого мы получим невыразимо уродливую ошибку «Uncaught Exception»:

  Неустранимая ошибка: необработанное исключение «MyNameSpaceMyException»
 с сообщением "ты дурак!"  в / home / lorna / sitepoint /
 OOP-функции / namespaced_typehints.php: 11 

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

И наконец

Предложение finally — это функция, о которой стоит знать, недавно появившаяся в PHP 5.5. Если вы использовали другие языки программирования с исключениями, возможно, вы уже видели эту конструкцию раньше. Есть только один блок try , мы можем иметь столько блоков catch сколько пожелаем, и из этой версии PHP мы также можем добавить finally .

Вот снова предыдущий пример кода, finally добавленный:

 <?php namespace MyNameSpace; class MyException extends Exception { } class MyClass { public function doSomething() { throw new MyException("you fool!"); } } try { $myclass = new MyClass(); $myclass->doSomething(); echo "that went well"; } catch (Exception $e) { echo "uh oh ... " . $e->getMessage(); } finally { echo "move along, nothing to see here"; } 

Предложение finally всегда будет происходить независимо от того, достигли ли мы конца блока try , вошли ли какие-либо из блоков catch или есть ли еще неучтенные исключения на пути. В этом примере вывод из блока finally действительно появляется перед ошибкой об исключении uncaught, потому что он не является uncaught, пока мы не завершим раздел try / catch / finally .

Вывод

Конечно, некоторые из приведенных здесь примеров являются надуманными, и, надеюсь, вы не будете сталкиваться с ними каждый день, но я думаю, что стоит знать, как выглядят крайние случаи, чтобы вы могли правильно проектировать и создавать системы. Понимание строительных блоков, безусловно, может помочь нам выполнить свою работу и быстро отладить проблемы, и мне нравится делиться идеями, чтобы сделать это проще!

Что ты нашел в PHP? Пожалуйста, поделитесь с остальными в разделе комментариев.

Изображение через Fotolia