PHP: Когда использовать трейт

Источник: «When to use a trait?»
Когда использовать трейт? Никогда. Для трейта всегда есть лучшая альтернатива. В любом случае на практике я всегда нахожу лучшие альтернативы трейту.

Ну, можно считать, что у трейта есть несколько преимуществ:

Преимущества трейта

  1. Если вы хотите повторно использовать некий повторяющийся код в нескольких классах, использования трейта является альтернативой для расширения класса. В этом случае трейт (trait) может быть лучшим вариантом, потому что он не становится частью иерархии типов, то есть класс, который использует трейт, не является экземпляром этого трейта.
  2. Трейт избавит вас от копирования/вставки, предлагая вместо этого копирование/вставку во время компиляции.

Недостатки трейта

С другой стороны есть несколько проблем с трейтами. Например:

  1. Когда трейт добавляет в класс один или несколько публичных методов, часто возникает необходимость определить эти методы, как интерфейс автоматически реализуемый с помощью трейта. Это невозможно. Трейт не может реализовать интерфейс. Таким образом, вы должны использовать трейт и явно реализовывать интерфейс, если хотите добиться этого.
  2. Трейты не имеют приватных методов и свойств. Мне нравится скрывать некоторые вещи от класса, в котором используется трейт, но невозможно реализовать их приватным трейтом. Всё, что определено, как приватное в трейте, будет доступно классу, в котором используется трейт. Что делает невозможным для трейта инкапсулировать что-либо.

Лучшие альтернативы

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

  1. Если у трейта есть служебные обязанности, лучше превратить его в реальную службу (service) внедряемую в качестве аргумента конструктора в службу, которая требует эту службу. Это известно как создание поведения, а не наследование поведения. Также, этот подход можно использовать, когда вы хотите избавиться от родительских классов.
  2. Если трейт добавляет сущности какое-то поведение, которые является одинаковым для другой сущности, часто имеет смысл ввести объект-значение и использовать его вместо трейта.
  3. Другой вариант, когда у вас одинаковое поведение в разных частях модели, — просто скопируйте код. Это позволит сохранить дизайн каждого объекта достаточно гибким, и каждый сможет развиваться в своём собственном направлении. Когда мы захотим изменить логику, на не придётся беспокоиться и других местах, которые повторно используют ту же логику.

Контрпример

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

trait EventRecording
{
/**
* @var list<object>
*/

private array $events = [];

private function recordThat(object $event): void
{
$this->events[] = $event;
}

/**
* @return list<object>
*/

public function releaseEvents(): array
{
$events = $this->events;

$this->events = [];

return $events;
}
}

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

Одна проблема может заключаться в том, что метод releaseEvents() является публичным. Но он необязательно должен быть в каком-либо интерфейсе. Нам не нужен интерфейс Entity или интерфейс RecordsEvents, если только мы не хотим создать повторно используемый код, который может сохранять сущность и отправлять её записанные события.

Другая проблема в том, что трейт страдает от отсутствия приватного трейта. Например, вместо $this->recordThat(...), сущность использующая этот трейт, может просто выполнить $this->events[] = .....

Мы могли бы решить эту проблему извлекая код в объект (добавляя ещё больше доказательств к утверждению, что всегда есть альтернатива трейту):

final class EventRecording
{
/**
* @var list<object>
*/

private array $events = [];

public function recordThat(object $event): void
{
// ...
}

/**
* @return list<object>
*/

public function releaseEvents(): array
{
// ...
}
}

Затем нам нужно присвоить экземпляр этого нового класса приватному свойству сущности:

final class SomeEntity
{
// Можем ли мы уже сделать это? Я забыл
private EventRecording $eventRecording = new EventRecording();

/**
* @return list<object>
*/

public function releaseEvents(): array
{
return $this->eventRecording->releaseEvents();
}

// ...
}

Каждой сущности по-прежнему нужен этот публичный метод releaseEvents() и это действительно дополнительно приватное свойство, которое мы копируем/вставляем в каждый новый класс сущности. Мы могли бы снова ввести трейт для этого:

trait ReleaseEvents
{
private EventRecording $eventRecording = new EventRecording();

/**
* @return list<object>
*/

public function releaseEvents(): array
{
return $this->eventRecording->releaseEvents();
}
}

Я чувствую, что извлечённый класс EventRecording в любом случае не слишком много делает и может считаться Ленивым классом (дурно пахнущий код). С таким же успехом мы могли бы использовать оригинальный трейт, принять проблемы дизайна и покончить с этим. Отлично.

Запрос Трейта

Основываясь на своём опыте работы с трейтами (я видел их примеры в проектах, фреймворках и библиотеках), я не знаю ни одного подходящего случая для использования трейтов, поэтому моё правило — никогда их не использовать. Но у вас, вероятно есть несколько хороших примеров трейтов, которые имеют смысл, и имеют смысл только как трейт. Итак, вот мой запрос трейта. Пожалуйста, поделитесь своими примерами в комментарии!

Дополнительные материалы

Предыдущая Статья

PHP 8.2: Что нового. Изменения и новый функционал.

Следующая Статья

PHP: Это DTO или Объект-Значение?