Поиск…
Черты для облегчения повторного использования горизонтального кода
Предположим, у нас есть интерфейс для ведения журнала:
interface Logger {
function log($message);
}
Теперь скажем, что у нас есть две конкретные реализации интерфейса Logger
: FileLogger
и ConsoleLogger
.
class FileLogger implements Logger {
public function log($message) {
// Append log message to some file
}
}
class ConsoleLogger implements Logger {
public function log($message) {
// Log message to the console
}
}
Теперь, если вы определите какой-то другой класс Foo
который вы также хотите выполнять для ведения журналов, вы можете сделать что-то вроде этого:
class Foo implements Logger {
private $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function log($message) {
if ($this->logger) {
$this->logger->log($message);
}
}
}
Foo
теперь также Logger
, но его функциональность зависит от реализации Logger
переданной ему через setLogger()
. Если теперь мы хотим, чтобы класс Bar
также имел этот механизм регистрации, нам пришлось бы дублировать эту логику в классе Bar
.
Вместо дублирования кода можно определить признак:
trait LoggableTrait {
protected $logger;
public function setLogger(Logger $logger) {
$this->logger = $logger;
}
public function log($message) {
if ($this->logger) {
$this->logger->log($message);
}
}
}
Теперь, когда мы определили логику в признаке, мы можем использовать признак, чтобы добавить логику в классы Foo
и Bar
:
class Foo {
use LoggableTrait;
}
class Bar {
use LoggableTrait;
}
И, например, мы можем использовать класс Foo
следующим образом:
$foo = new Foo();
$foo->setLogger( new FileLogger() );
//note how we use the trait as a 'proxy' to call the Logger's log method on the Foo instance
$foo->log('my beautiful message');
Решение конфликта
Попытка использовать несколько признаков в одном классе может привести к проблемам, связанным с конфликтующими методами. Вам необходимо разрешить такие конфликты вручную.
Например, давайте создадим эту иерархию:
trait MeowTrait {
public function say() {
print "Meow \n";
}
}
trait WoofTrait {
public function say() {
print "Woof \n";
}
}
abstract class UnMuteAnimals {
abstract function say();
}
class Dog extends UnMuteAnimals {
use WoofTrait;
}
class Cat extends UnMuteAnimals {
use MeowTrait;
}
Теперь давайте попробуем создать следующий класс:
class TalkingParrot extends UnMuteAnimals {
use MeowTrait, WoofTrait;
}
PHP-интерпретатор вернет фатальную ошибку:
Неустранимая ошибка: метод Trait сказать не применялся, потому что на TalkingParrot происходят столкновения с другими методами trait
Чтобы разрешить этот конфликт, мы могли бы сделать это:
- используйте ключевое слово
insteadof
чтобы использовать метод из одного признака вместо метода из другого признака - создать псевдоним для метода с конструкцией, подобной
WoofTrait::say as sayAsDog;
class TalkingParrotV2 extends UnMuteAnimals {
use MeowTrait, WoofTrait {
MeowTrait::say insteadof WoofTrait;
WoofTrait::say as sayAsDog;
}
}
$talkingParrot = new TalkingParrotV2();
$talkingParrot->say();
$talkingParrot->sayAsDog();
Этот код будет выдавать следующий результат:
мяу
гав
Использование нескольких признаков
trait Hello {
public function sayHello() {
echo 'Hello ';
}
}
trait World {
public function sayWorld() {
echo 'World';
}
}
class MyHelloWorld {
use Hello, World;
public function sayExclamationMark() {
echo '!';
}
}
$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();
Вышеприведенный пример выводит:
Hello World!
Изменение видимости метода
trait HelloWorld {
public function sayHello() {
echo 'Hello World!';
}
}
// Change visibility of sayHello
class MyClass1 {
use HelloWorld { sayHello as protected; }
}
// Alias method with changed visibility
// sayHello visibility not changed
class MyClass2 {
use HelloWorld { sayHello as private myPrivateHello; }
}
Выполнение этого примера:
(new MyClass1())->sayHello();
// Fatal error: Uncaught Error: Call to protected method MyClass1::sayHello()
(new MyClass2())->myPrivateHello();
// Fatal error: Uncaught Error: Call to private method MyClass2::myPrivateHello()
(new MyClass2())->sayHello();
// Hello World!
Поэтому имейте в виду, что в последнем примере в MyClass2
исходный не сглаженный метод из trait HelloWorld
остается доступным как есть.
Что такое черта?
PHP допускает только одно наследование. Другими словами, класс может extend
только один класс. Но что, если вам нужно включить что-то, что не принадлежит родительскому классу? До PHP 5.4 вам нужно будет проявить творческий подход, но в 5.4 были введены черты. Черты позволяют вам в основном «копировать и вставлять» часть класса в ваш основной класс
trait Talk {
/** @var string */
public $phrase = 'Well Wilbur...';
public function speak() {
echo $this->phrase;
}
}
class MrEd extends Horse {
use Talk;
public function __construct() {
$this->speak();
}
public function setPhrase($phrase) {
$this->phrase = $phrase;
}
}
Итак, у нас есть MrEd
, который уже расширяет Horse
. Но не все лошади Talk
, поэтому у нас есть черта для этого. Отметим, что это делает
Во-первых, мы определяем нашу черту. Мы можем использовать его с автозагрузкой и пространствами имен (см. Также ссылку на класс или функцию в пространстве имен ). Затем мы включили его в наш MrEd
класса с ключевым словом use
.
Вы заметите, что MrEd
использует функции Talk
и переменные без их определения. Помните, что мы говорили о копировании и вставке ? Теперь эти функции и переменные определены внутри класса, как если бы этот класс определил их.
Черты наиболее тесно связаны с абстрактными классами, поскольку вы можете определить переменные и функции. Вы также не можете создавать экземпляр напрямую (т. new Trait()
). Черты не могут заставить класс неявно определять функцию типа абстрактного класса или интерфейса. Черты только для явных определений (поскольку вы можете implement
столько интерфейсов, сколько хотите, см. Раздел Интерфейсы ).
Когда следует использовать черту?
Первое, что вы должны сделать, рассматривая Черту, - задать себе этот важный вопрос
Могу ли я использовать черту, реструктурируя свой код?
Чаще всего ответ будет Да . Черты являются краевыми случаями, вызванными одиночным наследованием. Искушение злоупотреблять или злоупотреблять чертами может быть высоким. Но учтите, что Trait вводит другой источник для вашего кода, а это означает, что есть еще один уровень сложности. В данном примере мы имеем дело только с 3 классами. Но черты означают, что теперь вы можете иметь дело гораздо больше. Для каждого Тренда ваш класс становится намного сложнее в работе, так как теперь вы должны обратиться к каждому значению, чтобы узнать, что он определяет (и, возможно, там, где произошло столкновение, см. Разрешение конфликтов ). В идеале, вы должны сохранить как можно меньше признаков в своем коде.
Черты для сохранения классов
Со временем наши классы могут внедрять все больше и больше интерфейсов. Когда эти интерфейсы имеют много методов, общее количество методов в нашем классе станет очень большим.
Например, предположим, что у нас есть два интерфейса и класс, реализующий их:
interface Printable {
public function print();
//other interface methods...
}
interface Cacheable {
//interface methods
}
class Article implements Cachable, Printable {
//here we must implement all the interface methods
public function print(){ {
/* code to print the article */
}
}
Вместо того, чтобы внедрять все методы интерфейса внутри класса Article
, мы могли бы использовать отдельные черты для реализации этих интерфейсов, сохраняя класс меньшим и разделяя код реализации интерфейса с классом.
Например, для реализации интерфейса Printable
мы могли бы создать этот признак:
trait PrintableArticle {
//implements here the interface methods
public function print() {
/* code to print the article */
}
}
и заставить класс использовать черту:
class Article implements Cachable, Printable {
use PrintableArticle;
use CacheableArticle;
}
Основными преимуществами были бы то, что наши методы реализации интерфейса будут отделены от остальной части класса и сохранены в признаке, который несет полную ответственность за реализацию интерфейса для этого конкретного типа объекта.
Внедрение Singleton с использованием черт
Отказ от ответственности : никоим образом этот пример не защищает использование синглетов. Синглтоны должны использоваться с большой осторожностью.
В PHP существует довольно стандартный способ реализации singleton:
public class Singleton {
private $instance;
private function __construct() { };
public function getInstance() {
if (!self::$instance) {
// new self() is 'basically' equivalent to new Singleton()
self::$instance = new self();
}
return self::$instance;
}
// Prevent cloning of the instance
protected function __clone() { }
// Prevent serialization of the instance
protected function __sleep() { }
// Prevent deserialization of the instance
protected function __wakeup() { }
}
Чтобы предотвратить дублирование кода, рекомендуется извлечь это поведение в черту.
trait SingletonTrait {
private $instance;
protected function __construct() { };
public function getInstance() {
if (!self::$instance) {
// new self() will refer to the class that uses the trait
self::$instance = new self();
}
return self::$instance;
}
protected function __clone() { }
protected function __sleep() { }
protected function __wakeup() { }
}
Теперь любой класс, который хочет функционировать как одноэлементный, может просто использовать признак:
class MyClass {
use SingletonTrait;
}
// Error! Constructor is not publicly accessible
$myClass = new MyClass();
$myClass = MyClass::getInstance();
// All calls below will fail due to method visibility
$myClassCopy = clone $myClass; // Error!
$serializedMyClass = serialize($myClass); // Error!
$myClass = deserialize($serializedMyclass); // Error!
Несмотря на то, что теперь невозможно сериализовать синглтон, все же полезно также запретить метод десериализации.