трейты и интерфейсы php
Как я использую трейты
Недавно несколько человек просили меня рассказать об использовании трейтов в новом проекте, который я пишу. Примерно в тоже время Рафаэль Домс показал мне его новую речь о сложных когнитивных процессах, которые мы не замечаем. Так как наш мозг — это большой мешок, перемешивающий все, в результате получился пост, который пытается охватить и то как я использую трейты, и то как я решаю где они нужны.
Воздействие vs Абстракция
Первое, что вы должны сделать — пойти почитать пост “Abstraction or Leverage” от Майкла Найгарда. Это отличная статья.
Если же у вас мало времени, основная суть поста состоит в том, что части кода (функции, классы, методы и т.д.) могут предназначаться либо для абстракции, либо для воздействия. Разница в:
Общим воздействием будет что-то вроде базового класса контроллера в вашем фреймворке. Он не скрывает реализацию, просто добавляет некоторые классные фичи, облегчающие работу.
В вышеупомянутом посте говорится, что как абстракции, так и воздействия хороши. Но абстракция — немного лучше, потому что она всегда дает вам возможность воздействовать, а воздействие не дает вам абстракции. Тем не менее, я хотел бы добавить, что хорошая абстракция является более трудозатратной в создании и не на всех уровнях возможна. Так что это компромисс.
Как это связано с трейтами?
Некоторые возможности языка лучше чем другие в создании либо воздействия, либо абстракции. Интерфейсы, например, отлично помогают нам строить и применять абстракции.
Наследование, с другой стороны, великолепно в предоставлении воздействия. Оно позволяет нам переопределить части родительского кода без необходимости копировать или извлекать каждый метод, использовать код класса (но не обязательно абстрактного) несколько раз. Так, отвечая на первоначальный вопрос, когда же я могу использовать трейты?
Я использую трейты, когда я хочу создать воздействие, не абстракцию.
Иногда?
Бенджамин Эберлей высказал хороший аргумент, что трейты имеют в основном те же проблемы, что и статический доступ. Вы не можете заменить или переопределить их, они откровенно плохо поддаются тестированию.
Но все же статические методы полезны. Если у вас одна функция без состояния и вы не хотите заменить ее на другую реализацию, то нет ничего плохого в том, чтобы сделать ее статической. Именованные конструкторы (вы же редко хотите именно пустой объект) или получение массива/результата математических операций с хорошо определенными вводом/выводом, без состояния, детерминированные: все это вам интересно. Статическое состояние, а не методы, вот реальное зло.
Трейты имеют примерно те же ограничения, плюс они могут быть использовании только внутри класса. Они более глобальны, чем объект.
Это дает трейтам дополнительную особенность: они могут работать (читать и писать) с внутренним состоянием класса, в который подмешаны. В некоторых случаях это делает их более подходящими чем статические методы.
Например, я часто использую генерацию доменных событий в сущности:
Мы можем сделать рефакторинг и превратить этот код в абстракцию, но это все равно будет хорошим примером того, как трейты могут работать с локальным состоянием объекта в отличии от статических методов. Мы не хотим работать с массивом событий вслепую или перемещать его из объекта. Возможно, мы не хотим добавлять еще одну абстракцию внутрь модели, и нам, конечно же, не хочется копипастить этот шаблонный код везде. И тут опять нам помогут трейты.
Другими практическими примерами могут служить настраиваемое логирование функций, дамп нескольких свойств сразу или общая итерационная/поисковая логика. Мы могли бы решить все эти задачи родительским классом, но поговорим об этом чуть позже.
Итак, трейты являются хорошей заменой в подобных случаях, но это не значит, что статические методы бесполезны. На самом деле, я все же предпочитаю использовать статические методы в случаях, когда не нужно изменять внутреннее состояние объекта, поскольку это всегда безопасней. Статические методы также намного удобнее в тестировании, не требуют mock класса.
Создание утверждений является хорошим примером тех случаев, где я предпочитаю статические методы, несмотря на то, что их обычно можно поместить в трейты. Я нахожу, что Assertion::positiveNumber($int) дает мне вышеупомянутые преимущества и мне легче понять что делает вызываемый класс.
Так что заявляю: Я использую трейты, когда я хочу воздействия, которому требуется доступ к внутреннему состоянию объекта.
Родительские классы
Все что мы перечислили также можно реализовать через наследование. В EventGeneratingEntity возможно такой подход был бы даже лучше, поскольку массив событий действительно будет индивидуальным. Однако, трейты дают возможность множественного наследования вместо одного базового класса. Помимо набора функций, есть ли еще хорошие аргументы за такой подход?
При прочих равных, я бы ориентировался на правило вроде «Является-A против Имеет-A». Конечно, это не точное правило, потому что трейты не являются композицией, но разумный ориентир.
Другими словами, родительские классы нужно использовать для функций, которые присуще какому-то объекту. Родительские классы хорошо передают другим разработчикам смысл кода: «Сотрудник — это человек». Если нам необходимо воздействие, это не означает, что код не должен быть коммуникативным.
Для другой непрофильной функциональности объекта (логирование, события и т.д.) трейты являются подходящим инструментом. Они не определяют характер класса, это вспомогательные функции, или еще лучше — деталь реализации. Все что вы получите от трейта должно находиться на службе у главной цели объекта, трейты не должны стать важной частью функциональности.
Так что, в случае генерации событий, я все-таки предпочту трейт, потому что создание событий — это вспомогательный функционал.
Интерфейсы
Я редко (если вообще) расширяю класс или создаю трейт без сопутствующего создания интерфейса.
Если вы будете следовать этому правилу, то обнаружите, что трейты хорошо дополняют принципы разделения интерфейсов. Легко определить интерфейс для дополнительной функциональности и сделать простую реализацию в трейте по умолчанию.
Это позволяет целевому классу реализовывать свою собственную версию интерфейса или использовать трейт по-умолчанию для неважных случаев. Если ваш выбор — это шаблоны и форсирование бедной абстракции, трейты могут быть мощным союзником.
И еще, если у вас только один реализующий интерфейс класс в коде и вы не планируете это менять, то имплементируйте функционал прямо в нем, не беспокойтесь о трейтах. Таким образом вы сделаете ваш код более читабельным и поддерживаемым.
Когда я не использую трейты
Честно говоря, я не использую трейты довольно часто, возможно раз в несколько месяцев я создаю трейт при постоянной работе над проектами. Вся эта эвристика, которую я обрисовывал (воздействие, требующее доступа к внутреннему состоянию), является крайне нишевой. Если вы используете их слишком часто, возможно вам нужно сделать шаг назад и пересмотреть свой стиль программирования. Есть хороший шанс, что тысячи классов только и ждут, чтобы быть реализованными.
Есть несколько мест, где я не люблю использовать трейты из-за стилевых предпочтений:
PHP :: Специальные виды классов
Абстрактные классы в PHP
В PHP поддерживается определение классов и методов, т.е. некоторых наиболее общих шаблонов, которые в основном имеют описательный смысл и задают характерные особенности всех наследуемых классов-потомков. При этом, если в классе объявляется хотя бы один абстрактный метод, то и сам класс должен быть объявлен абстрактным. Также нельзя создавать экземпляры (объекты) абстрактного класса, а методы, объявленные абстрактными, не должны включать конкретной реализации. Объявляются абстрактные классы и методы при помощи служебного слова abstract (см. пример №1 ).
Пример №1. Использование абстрактных классов и методов
Интерфейсы в PHP
Пример №2. Использование интерфейсов
Реализовывать в одном классе два интерфейса, содержащих одноименную функцию, запрещено, т.к. это повлечет за собой неоднозначность. Кроме того, сигнатуры методов в классе, реализующем интерфейс, должны точно совпадать с сигнатурами, используемыми в интерфейсе, иначе будет вызвана ошибка.
Отметим, что интерфейсы могут также содержать константы, которые работают так же, как и константы обычных классов, за исключением того, что они не могут быть переопределены наследующим классом или интерфейсом.
Трейты в PHP
Пример №3. Использование трейтов
При формировании трейтов разрешается использовать абстрактные методы, а также статические свойства и методы, но объявлять константы в трейтах нельзя.
Пример №4. Разрешение конфликтов в трейтах
Обратите внимание, что в примере трейт, который был использован в родительском классе, не переопределяет одноименный метод этого класса. Если же трейт вставляется в класс-потомок, то методы трейта, наоборот, имеют больший приоритет и переопределяют одноименные унаследованные методы родительского класса, хотя собственные методы текущего класса-потомка они по-прежнему не переопределяют.
Анонимные классы в PHP
В PHP 7 была добавлена поддержка классов, которые могут быть полезны в случае необходимости одноразового использования, например, для возврата функцией значения в виде определенного объекта с требуемым набором свойств и методов. При этом анонимные классы могут иметь конструкторы, расширять другие классы, реализовывать интерфейсы или подключать трейты точно также, как и обычные классы (см. пример №5 ).
Пример №5. Анонимные классы
Пример №6. Использование анонимных классов внутри других классов
Трейты и интерфейсы php
Интерфейсы объектов позволяют создавать код, который указывает, какие методы должен реализовать класс, без необходимости определять, как именно они должны быть реализованы. Интерфейсы разделяют пространство имён с классами и трейтами, поэтому они не могут называться одинаково.
Все методы, определённые в интерфейсах, должны быть общедоступными, что следует из самой природы интерфейса.
На практике интерфейсы используются в двух взаимодополняющих случаях:
Интерфейсы могут определять магические методы, требуя от реализующих классов реализации этих методов.
Хотя они поддерживаются, использование конструкторов в интерфейсах настоятельно не рекомендуется. Это значительно снижает гибкость объекта, реализующего интерфейс. Кроме того, к конструкторам не применяются правила наследования, что может привести к противоречивому и неожиданному поведению.
implements
Класс может реализовать два интерфейса, которые определяют метод с тем же именем, только если объявление метода в обоих интерфейсах идентично.
Класс, реализующий интерфейс, может использовать для своих параметров имя, отличное от имени интерфейса. Однако, начиная с PHP 8.0, в языке поддерживаются именованные аргументы, и вызывающий код может полагаться на имя параметра в интерфейсе. По этой причине настоятельно рекомендуется, чтобы разработчики использовали те же имена параметров, что и реализуемый интерфейс.
Интерфейсы могут быть унаследованы друг от друга, так же, как и классы, с помощью оператора extends.
Класс, реализующий интерфейс, должен объявить все методы в интерфейсе с совместимой сигнатурой.
Константы ( Constants )
Интерфейсы могут содержать константы. Константы интерфейсов работают точно так же, как и константы классов, за исключением того, что они не могут быть переопределены наследующим классом или интерфейсом.
Примеры
Пример #1 Пример интерфейса
Пример #2 Наследование интерфейсов
interface A
<
public function foo ();
>
// Это сработает
class C implements B
<
public function foo ()
<
>
// Это не сработает и выдаст фатальную ошибку
class D implements B
<
public function foo ()
<
>
Пример #3 Множественное наследование интерфейсов
interface A
<
public function foo ();
>
interface B
<
public function bar ();
>
class D implements C
<
public function foo ()
<
>
public function bar ()
<
>
Пример #4 Интерфейсы с константами
interface A
<
const B = ‘Константа интерфейса’ ;
>
// Выведет: Константа интерфейса
echo A :: B ;
// Это, однако, не будет работать, так как
// константы переопределять нельзя.
class B implements A
<
const B = ‘Константа класса’ ;
>
?>
Пример #5 Интерфейсы с абстрактными классами
Пример #6 Одновременное расширение и внедрение
Интерфейс, совместно с объявлениями типов, предоставляет отличный способ проверки того, что определённый объект содержит определённый набор методов. Смотрите также оператор instanceof и объявление типов.
Трейты в PHP
PHP поддерживает только одиночное наследование: дочерний класс не может наследовать от нескольких классов сразу, только от одного единственного родителя. Однако в большинстве случаев было бы полезно наследовать от нескольких классов. Например, было бы желательно наследовать методы от нескольких разных классов, чтобы предотвратить дублирование кода. Трейты используются для восполнения этого пробела, позволяя нам повторно использовать одни и те же свойства и методы в нескольких классах.
Что такое трейты?
Трейты (англ. trait) используются для объявления методов, которые можно использовать в нескольких классах. Трейты могут иметь методы и абстрактные методы, которые могут использоваться в нескольких классах. Методы трейтов могут иметь любой модификатор доступа (публичный, приватный или защищенный).
Синтаксис трейта такой же как и у класса, за исключением того, что имя трейта нужно объявлять с помощью ключевого слова trait :
Синтаксис
Экземпляр трейта, как и абстрактного класса, нельзя создать — трейты предназначены только для подключения к другим классам.
Синтаксис
Давайте посмотрим на пример в котором объявим один трейт и подключим к классу:
Пример
Результат выполнения кода:
По сути, трейт — это просто способ скопировать и вставить код во время выполнения.
Использование нескольких трейтов
Пример
Результат выполнения кода:
Чем трейты отличаются от интерфейсов?
Трейты очень похожи на интерфейсы. И трейты, и интерфейсы обычно просты, лаконичны и мало используются без реально реализованного класса. Однако разница между ними есть.
Интерфейс — это контракт, в котором говорится, что «этот объект может делать это», тогда как трейт дает объекту возможность делать это.
Другими словами, если код ООП касается планирования и проектирования, то интерфейс — это план, а объект — полностью построенный дом. Между тем, трейты — это просто способ помочь построить дом, спроектированный по плану (интерфейсу).
Интерфейсы — это спецификации, которые можно проверить используя оператор instanceof (является ли текущий объект экземпляром указанного класса).
Вы должны использовать трейты только тогда, когда несколько классов имеют одну и ту же функциональность (вероятно, продиктованную одним и тем же интерфейсом). Нет смысла использовать трейт для обеспечения функциональности для одного класса: это только запутывает то, что делает класс.
Пример
Результат выполнения кода:
Основное отличие состоит в том, что с интерфейсами вы должны определить фактическую реализацию каждого метода в каждом классе, реализующем указанный интерфейс, поэтому вы можете иметь множество классов, реализующих один и тот же интерфейс, но с различным поведением. В то время как трейты — это просто фрагменты кода, введенные в класс. Еще одно важное отличие состоит в том, что методы трейтов могут быть только методами класса или статическими методами, в отличие от методов интерфейса, которые также могут (и обычно являются) методами экземпляра.
Переопределение унаследованных методов
Как описано в Руководстве: Унаследованный метод от базового класса переопределяется методом, вставленным с помощью трейта. Порядок приоритета таков — методы текущего класса переопределяют методы трейта, которые в свою очередь переопределяют унаследованные методы.
Итак, рассмотрим следующий сценарий:
Пример
Результат выполнения кода:
При создании экземпляра MyClass, описанного выше, происходит следующее:
Заключение
Трейты в PHP
Как мы знаем, в PHP класс может наследоваться только от одного класса. Но как быть, если мы хотим иметь какой-либо функционал в разных классах, которые не являются наследниками друг друга? Для этого придумали трейты. Трейты в PHP – это такой механизм, который позволяет внутри классов избегать повторного использования кода.
Давайте разберём на примере. Пусть у нас будут два совершенно не связанных между собой класса: коробка и человек. Но предположим, что мы хотели бы, чтобы они оба умели говорить о том, какого они класса. Давайте для начала создадим два этих класса и прямо в них создадим методы sayYourClass(), которые будут выводить имя класса, объектами которого они являются.
В PHP можно получить имя класса с помощью конструкции ИмяКласса::class. Например:
Если мы находимся внутри класса, например, в каком-то его методе, то мы можем ИмяКласса заменить словом self – текущий класс.
Результат останется прежним.
Как видим, в классах получились одинаковые методы, и было бы неплохо избавиться от дублирования. Вот тут нам и понадобится трейт. Описываются трейты следующим образом:
Ничего сложного. Теперь мы можем просто использовать этот трейт в двух наших классах. Для этого используется конструкция use.
И снова увидим нужный результат:
Код из трейта SayYourClass просто подставился в классы, где мы его использовали с помощью слова use. В self будет лежать класс, в котором сейчас исполняется этот код. Вот так всё просто.
Трейты также довольно плотно пересекаются с темой интерфейсов.
Давайте добавим интерфейс, который будет обязывать классы иметь метод sayYourClass().
Теперь наши классы могут его реализовать – этот метод реализован в трейте, а трейт используется в классе.
Давайте посмотрим на получившийся код.
В PHP класс может наследоваться только от одного класса, помните? Так вот с помощью интерфейсов и трейтов мы можем это ограничение немного обойти. Мы теперь можем добавлять некоторый функционал в классы, которые не имеют какого-то общего поведения в целом. Но они при этом объединены одним интерфейсом. А проверять, реализует ли объект интерфейс, мы уже умеем – с помощью конструкции instanceof.
Таким образом, мы можем обходить ограничения наследования: