PHP: Когда использовать трейт
Ну, можно считать, что у трейта есть несколько преимуществ:
Преимущества трейта
- Если вы хотите повторно использовать некий повторяющийся код в нескольких классах, использования трейта является альтернативой для расширения класса. В этом случае трейт (trait) может быть лучшим вариантом, потому что он не становится частью иерархии типов, то есть класс, который использует трейт, не является
экземпляром этого трейта
. - Трейт избавит вас от копирования/вставки, предлагая вместо этого копирование/вставку во время компиляции.
Недостатки трейта
С другой стороны есть несколько проблем с трейтами. Например:
- Когда трейт добавляет в класс один или несколько публичных методов, часто возникает необходимость определить эти методы, как интерфейс автоматически реализуемый с помощью трейта. Это невозможно. Трейт не может реализовать интерфейс. Таким образом, вы должны использовать трейт и явно реализовывать интерфейс, если хотите добиться этого.
- Трейты не имеют
приватных
методов и свойств. Мне нравится скрывать некоторые вещи от класса, в котором используется трейт, но невозможно реализовать ихприватным трейтом
. Всё, что определено, как приватное в трейте, будет доступно классу, в котором используется трейт. Что делает невозможным для трейта инкапсулировать что-либо.
Лучшие альтернативы
В любом случае на практике я всегда нахожу лучшие альтернативы трейту. В качестве примера, вот несколько вариантов:
- Если у трейта есть служебные обязанности, лучше превратить его в реальную службу (service) внедряемую в качестве аргумента конструктора в службу, которая требует эту службу. Это известно как создание поведения, а не наследование поведения. Также, этот подход можно использовать, когда вы хотите избавиться от родительских классов.
- Если трейт добавляет сущности какое-то поведение, которые является одинаковым для другой сущности, часто имеет смысл ввести объект-значение и использовать его вместо трейта.
- Другой вариант, когда у вас одинаковое поведение в разных частях модели, — просто скопируйте код. Это позволит сохранить дизайн каждого объекта достаточно гибким, и каждый сможет развиваться в своём собственном направлении. Когда мы захотим изменить логику, на не придётся беспокоиться и других местах, которые повторно используют ту же логику.
Контрпример
Несмотря на то, что в большинстве ситуаций трейт на самом деле не нужен, и есть лучшие альтернативы, есть одна ситуация для которой я продолжаю использовать трейт. Вот код этого трейта, если вы когда-нибудь посещали один из моих семинаров, вы уже знаете его:
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
в любом случае не слишком много делает и может считаться Ленивым классом (дурно пахнущий код). С таким же успехом мы могли бы использовать оригинальный трейт, принять проблемы дизайна и покончить с этим. Отлично.
Запрос Трейта
Основываясь на своём опыте работы с трейтами (я видел их примеры в проектах, фреймворках и библиотеках), я не знаю ни одного подходящего случая для использования трейтов, поэтому моё правило — никогда их не использовать. Но у вас, вероятно есть несколько хороших примеров трейтов, которые имеют смысл, и имеют смысл только как трейт. Итак, вот мой запрос трейта. Пожалуйста, поделитесь своими примерами в комментарии!