Минимизация дублирования кода за счет лучшей организации и повторного использования кода является важной целью объектно-ориентированного программирования. Но в PHP это иногда может быть затруднено из-за ограничений модели одиночного наследования, которую он использует; у вас могут быть некоторые методы, которые вы хотели бы использовать в нескольких классах, но они могут не вписываться в иерархию наследования.
Такие языки, как C ++ и Python, позволяют нам наследовать от нескольких классов, что в некоторой степени решает эту проблему, а миксины в Ruby позволяют смешивать функциональность одного или нескольких классов без использования наследования. Но множественное наследование имеет такие проблемы, как проблема алмазной проблемы, и миксины могут быть сложным механизмом для работы.
В этой статье я расскажу о чертах, новой функции, представленной в PHP 5.4 для преодоления таких проблем. Само понятие черт не является чем-то новым для программирования и используется в других языках, таких как Scala и Perl. Они позволяют нам повторно использовать код по горизонтали между независимыми классами в разных иерархиях классов.
Как выглядит черта
Черта похожа на абстрактный класс, который не может быть реализован сам по себе (хотя чаще он сравнивается с интерфейсом). Документация PHP определяет черты следующим образом:
Черты — это механизм повторного использования кода в языках с одним наследованием, таких как PHP. Черта предназначена для уменьшения некоторых ограничений одиночного наследования, позволяя разработчику свободно повторно использовать наборы методов в нескольких независимых классах, находящихся в разных иерархиях классов.
Давайте рассмотрим этот пример:
<?php class DbReader extends Mysqli { } class FileReader extends SplFileObject { }
Было бы проблемой, если бы оба класса нуждались в некоторой общей функциональности, например, сделав их обоих одиночными. Поскольку PHP не поддерживает множественное наследование, либо каждый класс должен будет реализовать необходимый код для поддержки шаблона Singleton, либо будет иерархия наследования, которая не имеет смысла. Черты предлагают решение именно такой проблемы.
<?php trait Singleton { private static $instance; public static function getInstance() { if (!(self::$instance instanceof self)) { self::$instance = new self; } return self::$instance; } } class DbReader extends ArrayObject { use Singleton; } class FileReader { use Singleton; }
Черта Singleton
имеет прямую реализацию шаблона Singleton со статическим методом getInstance()
который создает объект класса с использованием этой черты (если он еще не создан) и возвращает его.
Попробуем создать объекты этих классов с помощью метода getInstance()
.
<?php $a = DbReader::getInstance(); $b = FileReader::getInstance(); var_dump($a); //object(DbReader) var_dump($b); //object(FileReader)
Мы можем видеть, что $a
является объектом DbReader
а $b
является объектом FileReader
, но оба они теперь ведут себя как одиночные. Метод из Singleton
был горизонтально введен в классы, использующие его.
Черты не накладывают никакой дополнительной семантики на класс. В некотором смысле вы можете думать об этом как о механизме копирования и вставки с помощью компилятора, где методы черты копируются в класс составления.
Если бы мы просто DbReader
от родительского DbReader
с частным свойством $instance
, это свойство не было бы показано в дампе ReflectionClass::export()
. И все же с чертами, это так!
Class [class FileReader] { @@ /home/shameer/workplace/php54/index.php 19-22 - константы [0] { } - Статические свойства [1] { Свойство [частный статический $ _instance] } - Статические методы [1] { Метод [статический публичный экземпляр метода] { @@ /home/shameer/workplace/php54/index.php 6 - 11 } } - Свойства [0] { } - Методы [0] { } }
Несколько черт
До сих пор мы использовали только одну черту с классом, но в некоторых случаях нам может потребоваться включить функциональность более чем одной черты.
<?php trait Hello { function sayHello() { echo "Hello"; } } trait World { function sayWorld() { echo "World"; } } class MyWorld { use Hello, World; } $world = new MyWorld(); echo $world->sayHello() . " " . $world->sayWorld(); //Hello World
Здесь у нас есть две черты, Hello
и World
. Черта Hello
может говорить только «Hello», а черта World
может говорить «World». В классе MyWorld
мы применили Hello
и World
так что объект MyWorld
будет иметь методы обоих признаков и иметь возможность сказать «Hello World».
Черты, составленные из черт
По мере роста приложения вполне возможно, что у нас будет набор черт, которые используются в разных классах. PHP 5.4 позволяет нам иметь черты, составленные из других черт, так что мы можем включить только одну вместо количества черт во все эти классы. Это позволяет нам переписать предыдущий пример следующим образом:
<?php trait HelloWorld { use Hello, World; } class MyWorld { use HelloWorld; } $world = new MyWorld(); echo $world->sayHello() . " " . $world->sayWorld(); //Hello World
Здесь мы создали черту HelloWorld
, используя черты Hello и World, и включили ее в MyWorld
. Поскольку черта HelloWorld
имеет методы из двух других черт, это точно так же, как если бы мы сами включили две черты в класс.
Порядок приоритета
Как я уже упоминал, черты работают так, как будто их методы были скопированы и вставлены в классы, использующие их, и они полностью сведены в определение классов. Могут быть методы с одинаковыми именами в разных чертах или в самом классе. Вы можете спросить, какой из них будет доступен в объекте дочернего класса.
Порядок приоритета:
- методы черты переопределяют унаследованные методы из родительского класса
- методы, определенные в текущем классе, переопределяют методы из черты
Это ясно из следующего примера:
<?php trait Hello { function sayHello() { return "Hello"; } function sayWorld() { return "Trait World"; } function sayHelloWorld() { echo $this->sayHello() . " " . $this->sayWorld(); } function sayBaseWorld() { echo $this->sayHello() . " " . parent::sayWorld(); } } class Base { function sayWorld(){ return "Base World"; } } class HelloWorld extends Base { use Hello; function sayWorld() { return "World"; } } $h = new HelloWorld(); $h->sayHelloWorld(); // Hello World $h->sayBaseWorld(); // Hello Base World
У нас есть класс HelloWorld
sayWorld()
от Base
, и оба класса имеют метод sayWorld()
но с разными реализациями. Также мы включили черту Hello
в класс HelloWorld
.
У нас есть два метода, sayHelloWorld()
и sayBaseWorld()
, первый из которых вызывает sayWorld()
который существует как в классах, так и в признаке. Но в выводе мы можем видеть, что был вызван дочерний класс. Если нам нужно сослаться на метод из родительского класса, мы можем сделать это, используя ключевое слово parent, как показано в sayBaseWorld()
.
Разрешение конфликтов и алиасинг
При использовании нескольких черт может возникнуть ситуация, когда разные черты используют одинаковые имена методов. Например, PHP выдаст фатальную ошибку, если вы попытаетесь запустить следующий код из-за конфликтующих имен методов:
<?php trait Game { function play() { echo "Playing a game"; } } trait Music { function play() { echo "Playing music"; } } class Player { use Game, Music; } $player = new Player(); $player->play();
Такие конфликты черт не разрешаются автоматически для вас. Вместо этого вы должны выбрать, какой метод следует использовать внутри класса составления, используя ключевое слово insteadof
.
<?php class Player { use Game, Music { Music::play insteadof Game; } } $player = new Player(); $player->play(); //Playing music
Здесь мы решили использовать метод play()
свойства Music
внутри класса композитинга, чтобы класс Player
воспроизводил музыку, а не игру.
В приведенном выше примере один метод был выбран по сравнению с другим из двух признаков. В некоторых случаях вы можете оставить их обоих, но при этом избегать конфликтов. Можно ввести новое имя для метода в качестве псевдонима. Псевдоним не переименовывает метод, но предлагает альтернативное имя, по которому он может быть вызван. Псевдонимы создаются с использованием ключевого слова as
.
<?php class Player { use Game, Music { Game::play as gamePlay; Music::play insteadof Game; } } $player = new Player(); $player->play(); //Playing music $player->gamePlay(); //Playing a game
Теперь любой объект класса Player
будет иметь метод gamePlay()
, который совпадает с Game::play()
. Здесь следует отметить, что мы разрешали любые конфликты явно, даже после наложения имен.
отражение
Reflection API — это одна из мощных функций PHP для анализа внутренней структуры интерфейсов, классов и методов и их обратного инжиниринга. И так как мы говорим о чертах, вам может быть интересно узнать о поддержке черт API Reflection. В PHP 5.4 четыре метода были добавлены в ReflectionClass
для получения информации о признаках в классе.
Мы можем использовать ReflectionClass::getTraits()
чтобы получить массив всех признаков, используемых в классе. Метод ReflectionClass::getTraitNames()
просто возвращает массив имен признаков в этом классе. ReflectionClass::isTrait()
полезен, чтобы проверить, является ли что-то чертой или нет.
В предыдущем разделе мы обсуждали наличие псевдонимов для признаков, чтобы избежать коллизий из-за признаков с тем же именем. ReflectionClass::getTraitAliases()
вернет массив псевдонимов признаков, сопоставленных с его исходным именем.
Другие преимущества
Помимо вышеупомянутого, есть и другие особенности, которые делают черты более интересными. Мы знаем, что в классическом наследовании частные свойства класса не могут быть доступны дочерним классам. Черты могут получить доступ к закрытым свойствам или методам классов-составителей, и наоборот! Вот пример:
<?php trait Message { function alert() { echo $this->message; } } class Messenger { use Message; private $message = "This is a message"; } $messenger = new Messenger; $messenger->alert(); //This is a message
Поскольку признаки полностью сведены в класс, составленный из них, любое свойство или метод признака станет частью этого класса, и мы получим к ним доступ, как и к любым другим свойствам или методам класса.
Мы можем даже иметь абстрактные методы в качестве черты, чтобы заставить составляющий класс реализовать эти методы. Например:
<?php trait Message { private $message; function alert() { $this->define(); echo $this->message; } abstract function define(); } class Messenger { use Message; function define() { $this->message = "Custom Message"; } } $messenger = new Messenger; $messenger->alert(); //Custom Message
Здесь у нас есть черта Message
с абстрактным методом define()
. Требуются все классы, которые используют эту черту для реализации метода. В противном случае PHP выдаст ошибку о том, что существует абстрактный метод, который не был реализован.
В отличие от черт в Scala, черты в PHP могут иметь конструктор, но он должен быть объявлен как общедоступный (будет выброшена ошибка, если она частная или защищенная) В любом случае, будьте осторожны при использовании конструкторов в чертах, потому что это может привести к непреднамеренным конфликтам в составных классах.
Резюме
Черты — одна из самых мощных функций, представленных в PHP 5.4, и я рассмотрел почти все их функции в этой статье. Они позволяют программистам повторно использовать фрагменты кода по горизонтали между несколькими классами, которые не обязательно должны находиться в одной иерархии наследования. Вместо сложной семантики они предоставляют нам легкий механизм повторного использования кода. Хотя есть некоторые недостатки с особенностями, они, безусловно, могут помочь улучшить дизайн вашего приложения, удалив дублирование кода и сделав его более СУХИМЫМ.
Изображение через Vlue / Shutterstock