<rmcreative>

RSS

Зло ли трейты?

7 мая 2017

Когда я стартовал кампанию на Patreon, то пообещал отвечать на вопросы. Первый вопрос задал Daniel и он о трейтах PHP:

Что думаете о трейтах в PHP? Зло ли они? Стоит ли их избегать? Если их использовать, какие особенности? Когда их лучше не использовать? Может был отрицательный или положительный опыт?

Что такое трейты?

В документации PHP трейты описываются так:

Трейт (англ. trait) — это механизм обеспечения повторного использования кода в языках с поддержкой единого наследования, таких как PHP. Трейт предназначен для уменьшения некоторых ограничений единого наследования, позволяя разработчику повторно использовать наборы методов свободно, в нескольких независимых классах и реализованных с использованием разных архитектур построения классов. Семантика комбинации трейтов и классов определена таким образом, чтобы снизить уровень сложности, а также избежать типичных проблем, связанных с множественным наследованием и смешиванием (mixins). > Трейт очень похож на класс, но предназначен для группирования функционала хорошо структурированым и последовательным образом. Невозможно создать самостоятельный экземпляр трейта. Это дополнение к обычному наследованию и позволяет сделать горизонтальную композицию поведения, то есть применение членов класса без необходимости наследования.

Описание длинное. Если по-простому, не затрагивая вопросы наследования, то трейты — такой умный копипаст.

<?php
trait Dumper
{
    public function dump($var)
    {
        echo '<pre>' . print_r($var, true) . '</pre>';
    }
}
 
class MyClass
{
    use Dumper;
}
 
$myClass = new MyClass();
$myClass->dump('test');

Код выше будет работать так же, как и следующий код:

<?php
class MyClass
{
    public function dump($var)
    {
        echo '<pre>' . print_r($var, true) . '</pre>';
    }
}

Трейты и Yii

Yii использует трейты для низкоуровневых штук и общего кода из слоя работы с базами данных. Трейты рассматривались как альтернатива поведениям в 2.0 и будут ещё раз рассматриваться для релиза 2.1.

Поведения

Я начал присматриваться к трейтам в августе 2010 когда их смёржили в trunk PHP (так зовётся master в CVS и SVN). К тому времени в Yii 1.x уже были поведения. Они похожи на mixin в Ruby и полезны для придания дополнительных возможностей, таких как SoftDeleteable или Versionable, существующим объектам или классам. Главное отличие в том, что у поведений есть своё состояние и они могут быть присоединены к объекту или отключены во время исполнения. У трейтов своего состояния нет.

Трейты ушли в релиз до выхода Yii 2.0, поэтому мы рассматривали их как альтернативу поведениям из Yii 1.1. Было решено оставить поведения. В то время не было хороших идей как сделать трейты настраиваемыми и как реализовать подключение и отключение их во время выполнения.

При формировании планов на версию 2.1 идея всплыла повторно. Мы нашли как реализовать настраиваемость трейта через использования абстрактных методов. Подключение и отключение во время выполнения оказалось невозможным, но нашёлся способ включать и выключать обработчики подписываясь на события или отписываясь от них. Финальное решение пока ещё не принято...

Низкоуровневая функциональность

Yii использует трейты для общей низкоуровневой функциональности: - ArrayableTrait - ArrayAccessTrait

Это самое очевидное и правильное использование трейтов.

Общий код слоя для работы с базами данных

Менее очевидно использование трейтов в слое для работы с базами данных: - ActiveQueryTrait - ActiveRelationTrait - QueryTrait - SchemaBuilderTrait - ViewFinderTrait

Интерфейс этого слоя в Yii 2.0 одинаков как для реляционных баз, так и для noSQL. Общие части реализации вынесены в трейты для того, чтобы не раздувать дерево наследования и не перекрывать слишком много методов для noSQL, значительно отличающихся от методов для традиционных баз данных. Лично я не очень доволен этой реализацией, если смотреть только на верность ООП. Можно было использовать композицию и обычные классы. Код был бы более тестируемым и читать его было бы проще. Тем не менее, использование трейтов оправдано так как оно значительно сокращает количество вызываемых методов и делает слой работы с базами данных производительней. С тестированием мы также разобрались несмотря на то, что это было не тривиально.

Особенности

Если вы задумались об использовании трейтов, подумайте дважды.

Отсутствие инкапсуляции

При использовании трейтов необходимо понимать, что всё содержимое трейта магически копируется в класс в изначальном виде. Конечно, вы можете разруливать конфликты имён, но это ничего не меняет. Именно поэтому в трейты не стоит выносить слишком много кода.

Идеально использовать трейты для небольшого количества относительно простого кода.

Состояние

Так как в трейтах нет нормальной инкапсуляции, для того, чтобы хранить состояние приходится либо добавлять приватные поля в трейт, либо заводить статические переменные в методах. И то и то может привести к проблемам.

Приватные поля трейта копируются в класс, так что возможны как конфликты имён, так и случайная модификация полей. Так как во время выполнения всё копируется в класс, класс имеет доступ даже к приватным полям.

Использование static означает, что все объекты класса, использующие трейт, будут иметь общее состояние, что необходимо далеко не всегда.

Так зло ли трейты?

Зависит от того, как вы их используете.

Как и в случае с копи-пейстом, зло относительно. Нет ничего абсолютно злого или абсолютно хорошего.

Трейты полезны для повторного использования реализации инетерфейса без необходимости заводить базовый класс. Во всех остальных случаях лучше воспользоваться композицией объектов.

Почитать

Комментарии RSS

  1. №10993
    Insolita
    Insolita 07 мая 2017 г., 19:54:33

    Поведения в YII полифункциональны - используются в качестве комплексных аттачей на события (в частности для АР), иногда добавляют доп. методы, иногда и то и то сразу. Если совсем от них отказываться то надо какую-то event-subscriber-dispatcher альтернативу.

  2. №10994
    Иван
    Иван 07 мая 2017 г., 22:34:31

    Подписался, чтобы получит приглашение на вебинары за деньги видимо. Но так и нет ни сроков, ни цен. Ничего.

  3. №10995
    Sam
    Sam 08 мая 2017 г., 13:21:17

    Иван, на что подписались?

  4. №10996
    Иван
    Иван 08 мая 2017 г., 13:27:00

    Я не помню уже. Ооп, yii2 вроде

  5. №10997
    Иван
    Иван 08 мая 2017 г., 13:27:34

    yi2 ооа

  6. №10998
    Insolita
    Insolita 08 мая 2017 г., 15:04:33

    Иван, вы похоже блогом ошиблись - www.elisdn.ru/blog

  7. №10999
    Виктор
    Виктор 12 мая 2017 г., 11:39:30

    Трейты хорошо идут вместе с интерфейсами.

    Еще в сущностях часто поля одинаковые, в так называемых таблицах словарях. Чтобы не писать геттеры, сеттеры, можно вынести в трейты.

  8. №11001
    Mendel
    Mendel 15 мая 2017 г., 15:00:35

    Есть еще одно применение трейтам. Когда по логике абстракций у нас должен быть один класс, но он или уж очень объемный выходит, или в нем явно прослеживаются разные ответственности, но при этом недостаточно явно чтобы строить дерево наследования, да и если строить то в каком порядке? Просто разбираем класс на несколько трейтов. Чисто чтобы уменьшить размер файла и улучшить читаемость. Особенно если у нас какой-то сахарный класс у которого реальной логики нет, а лишь перевызовы других вещей ради удобства. Например абстрактный контроллер у которого и вьювы дергаются и редиректы и куча всего. Получается класс килобайт на 15. Бегло просмотреть его - тяжко. А тут в начале юз, имена трейтов говорят сами за себя. В классе самые важные/общие фичи. что интересно - посмотрел в нужном трейте. Ну и конечно горизонтальный перенос генов между близкими но разными ветками наследования, как в ДАО. А всё остальное лучше не использовать. Уж очень оно конфликтное.

  9. №11002
    Виктор
    Виктор 19 мая 2017 г., 15:08:36

    Было бы круто, если бы трейты принимали параметры. Тогда можно было бы здоровую часть кода оптимизировать.

  10. №12282
    Сергей
    Сергей 05 июля 2023 г., 1:42:19

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

  1. Почта опубликована не будет.

  2. Можно использовать синтаксис Markdown или HTML.

  3. Введите ответ в поле. Щёлкните, чтобы получить другую задачу.