Недавно несколько человек просили меня рассказать об использовании трейтов в новом проекте, который я пишу. Примерно в тоже время Рафаэль Домс показал мне его новую речь о сложных когнитивных процессах, которые мы не замечаем. Так как наш мозг — это большой мешок, перемешивающий все, в результате получился пост, который пытается охватить и то как я использую трейты, и то как я решаю где они нужны.

Воздействие vs Абстракция


Первое, что вы должны сделать — пойти почитать пост “Abstraction or Leverage” от Майкла Найгарда. Это отличная статья.

Если же у вас мало времени, основная суть поста состоит в том, что части кода (функции, классы, методы и т.д.) могут предназначаться либо для абстракции, либо для воздействия. Разница в:

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

Общей абстракцией будет паттерн Репозиторий: вы не знаете как объект хранится или где, вам все равно. Детали лежат вне концепции Репозитория.

Общим воздействием будет что-то вроде базового класса контроллера в вашем фреймворке. Он не скрывает реализацию, просто добавляет некоторые классные фичи, облегчающие работу.

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

Как это связано с трейтами?


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

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

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

Иногда.

Иногда?


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

Но все же статические методы полезны. Если у вас одна функция без состояния и вы не хотите заменить ее на другую реализацию, то нет ничего плохого в том, чтобы сделать ее статической. Именованные конструкторы (вы же редко хотите именно пустой объект) или получение массива/результата математических операций с хорошо определенными вводом/выводом, без состояния, детерминированные: все это вам интересно. Статическое состояние, а не методы, вот реальное зло.

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

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

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

trait GeneratesDomainEvents
{
    private $events = [];

    protected function raise(DomainEvent $event)
    {
        $this->events[] = $event;
    }

    public function releaseEvents()
    {
        $pendingEvents = $this->events;
        $this->events = [];
        return $pendingEvents;
    }
}

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

Другими практическими примерами могут служить настраиваемое логирование функций, дамп нескольких свойств сразу или общая итерационная/поисковая логика. Мы могли бы решить все эти задачи родительским классом, но поговорим об этом чуть позже.

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

Создание утверждений является хорошим примером тех случаев, где я предпочитаю статические методы, несмотря на то, что их обычно можно поместить в трейты. Я нахожу, что Assertion::positiveNumber($int) дает мне вышеупомянутые преимущества и мне легче понять что делает вызываемый класс.

Если у вас есть подобные методы утверждений, всегда возникает соблазн превратить их в трейты, но подобный код уже начинает «попахивать». Возможно метод требует несколько параметров и вы устали их передавать. Возможно проверка $this->foo требует значения $this->bar. В любом из этих случаев рефакторинг класса будет лучшей альтернативой. Помните, всегда лучше, если воздействие уступает место абстракции.

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

Родительские классы


Все что мы перечислили также можно реализовать через наследование. В EventGeneratingEntity возможно такой подход был бы даже лучше, поскольку массив событий действительно будет индивидуальным. Однако, трейты дают возможность множественного наследования вместо одного базового класса. Помимо набора функций, есть ли еще хорошие аргументы за такой подход?

При прочих равных, я бы ориентировался на правило вроде «Является-A против Имеет-A». Конечно, это не точное правило, потому что трейты не являются композицией, но разумный ориентир.

Другими словами, родительские классы нужно использовать для функций, которые присуще какому-то объекту. Родительские классы хорошо передают другим разработчикам смысл кода: «Сотрудник — это человек». Если нам необходимо воздействие, это не означает, что код не должен быть коммуникативным.

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

Так что, в случае генерации событий, я все-таки предпочту трейт, потому что создание событий — это вспомогательный функционал.

Интерфейсы


Я редко (если вообще) расширяю класс или создаю трейт без сопутствующего создания интерфейса.

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

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

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

Когда я не использую трейты


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

Есть несколько мест, где я не люблю использовать трейты из-за стилевых предпочтений:

  • Если код, с которым вы работаете — это просто пара геттеров и сеттеров, я бы не стал заморачиваться. IDE могут быть тут хорошим козырем, а добавление трейта оставит после себя только снижение читаемости.
  • Не используйте трейты для внедрения зависимостей. Не столько из-за особенностей трейтов, сколько из-за особенностей сеттеров зависимостей, я против этого.
  • Мне не нравится использования трейтов в больших общедоступных API или крупных кусках функциональности. Пропустите этап воздействия и переходите непосредственно к абстракции.

И наконец, следует помнить, что трейты не предполагают абстракцию и они не являются композицией, но все равно имеют право занять место среди ваших инструментов. Они полезны для предоставления воздействия по-умолчанию при более мелких реализацию или дублировании кода. Всегда будьте готовы реорганизовать их для лучшей абстракции, как только почувствуете признаки кода «c запашком».

Комментарии (31)


  1. itcoder
    25.05.2015 17:01
    +3

    Статья актуальная, но примеров кода маловато, хотелось бы побольше примеров применения из жизни.
    Мне вместо трейтов больше нравятся поведения, их и тестами покрывать легче и поведение объекта более предсказуемо.


    1. Fesor
      27.05.2015 08:41

      Мне вместо трейтов больше нравятся поведения

      Я правильно понимаю что вы говорите это в контексте Yii? Если так, то тут есть очень большая разница, трэйты != миксины, а поведения в Yii имеют претензии на то что бы называться миксинами.

      примеров кода маловато

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


  1. hell0w0rd
    25.05.2015 19:40
    +1

    github.com/nkt/doctrine-columns — в тему. Трейты для популярных полей, при работе с ORM doctrine. Собственно либа демонстрирует подход, а не навязывает свое использование.
    Статья о библиотеке на хабре


    1. psylosss
      26.05.2015 11:31
      +1

      Это не оно же, только нативное? doctrine-orm.readthedocs.org/en/latest/tutorials/embeddables.html


      1. Fesor
        27.05.2015 08:38

        Не совсем, embeddables намного более мощная штука. Ну и появилась она не так давно.


    1. Fesor
      27.05.2015 08:37

      Ужасная идея агитирующая делать анемичную модель. Если у вас есть типичные поля, лучше использовать объекты-значения.


      1. Fesor
        27.05.2015 08:43

        Все же поясню, как и говорится в статье, сущности это не то место где стоит использовать трэйты. В этом репозитории трейты по сути нужны только для того, что бы не писать геттеры/сеттеры. Как по мне лучше пусть IDE сгенерирует это дело. А еще лучше — вообще отказаться от использования сеттеров.


        1. chetzof
          27.05.2015 08:58

          Есть опыт использования подобных без-сеттерных сущностей в контексте sf2? И не создает ли это больше проблем чем решает?


          1. Fesor
            27.05.2015 09:07

            В моем случае не создает вообще никаких проблем. Единственная проблема — необходимость использовать DTO (можно просто массивчики) и связанные с этим ограничения при работе с symfony/forms. Но я пишу апишки и там формы не нужны. Да и не сказал бы что это такой уж недостаток, он в принципе полезный. Ограничивает разработчика и явно отделяет данные запроса от данных модели, но на небольших проектах не удобно.


  1. Ostrovski
    25.05.2015 22:28
    +1

    Обычно использовал трейты, как такой инструмент для избавления от копи-паста, пока «правильная архитектура» еще не созрела (или временно невозможна из-за необходимости большого рефакторинга).
    P.S. Идея мириться со статическими методами, когда не нужно состояние, при возможности написать обычную функцию (за исключением порождающих паттернов), в очередной раз наталкивает меня на мысль, что PHP все больше начинает напоминать монструозную Java, где ни строки без класса не напишешь.


    1. Rathil
      26.05.2015 01:36

      Что-то я Вас не понял. А кто Вам не даёт писать простые ф-ции, вместо хелперов?


      1. Ostrovski
        26.05.2015 09:14

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


        1. Rathil
          26.05.2015 10:21
          +1

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


          1. Fesor
            27.05.2015 09:11

            Ну справедливости ради, можно в composer.json все это прописать и тогда никаких проблем с автозагрузкой.


            1. Rathil
              27.05.2015 10:41

              Если у меня будет 300 ф-ций и каждая будет лежать в отдельном файле (скажем не я писал этот код и он так реализован), Вы предлагаете ВСЕГДА подключать эти 300 файлов, даже если мне нужно будет только 16 ф-ций???


              1. Fesor
                27.05.2015 10:52

                Да, почему бы и нет. opcache все положит в разделяемую память, обращения к файловой системе можно нивелировать (просто отключить у opcache инвалидацию кэша).


                1. Rathil
                  27.05.2015 11:13

                  Ваше право, делайте как хотите :)


                  1. Fesor
                    27.05.2015 12:15

                    ну как минимум я не стану плодить 300 функций и уж тем более не буду ложить это в отдельные файлы. Обычно это пара файликов только с самым необходимым. В любом случае это лучше чем классы-утилиты.


                    1. Rathil
                      27.05.2015 13:21

                      Я не понимаю — чем лучше?


                      1. Ostrovski
                        27.05.2015 14:25
                        +1

                        Тем что класс — это сущность, порождающая объекты, а не namespace для функций. А весь этот спор — как раз явный показатель того, что у языка с этим проблемы.


                        1. Rathil
                          27.05.2015 14:46

                          Не считаю, что этот ответ отвечает на вопрос «ЧЕМ лучше?».


                          1. Ostrovski
                            27.05.2015 14:54

                            Имхо, это как: «Гвозди можно забивать молотком, а можно пассатижами. Молоток придумали и спроектировали, чтобы забивать гвозди, а пассатижи — чтобы делать куда более сложные манипуляции. Вопрос — чем лучше забивать гвозди и почему?»


                            1. Rathil
                              27.05.2015 15:15

                              Знаете, что-то мне подсказывает, что Вы все-же сами пишете хелперы, а не набор ф-ций. Конечно я могу ошибаться, но думаю что многие в команде будут против подключать файлы с ф-циями руками, а прописывать это в композере — это и есть «забивать гвозди пассатижами».


                              1. Ostrovski
                                27.05.2015 15:41

                                Конечно, в итоге в обоих случаях приходится забивать гвозди пассатижами.


                              1. Fesor
                                27.05.2015 22:02

                                а прописывать это в композере — это и есть «забивать гвозди пассатижами».

                                Аргументируйте, что противоестественного в этом? Что вас в этом пугает? Руками ничего дергать не надо, по сути все та же автоматическая загрузка функций, просто не по требованию. Казалось бы должно бы медленнее работать, но нет, opcache нам в этом поможет. Мне серьезно интересно, возможно я каких-то проблем не вижу…

                                Что до вашего примера с 300-ми файлами, если это зависимости — бог с ним (хотя это нереально много и очень не реалистичный кейс). Если же речь идет о функциях-хелперах в составе вашего проекта, то тут возникает вопрос… а зачем вам столько и почему они каждый в отдельном файле?


                                1. Rathil
                                  28.05.2015 00:53

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


                                  1. Fesor
                                    28.05.2015 01:03

                                    в настройку зависимостей (композер).

                                    то есть, если следовать вашей логикой, вы пишите свои автолоадеры для классов вместо того что бы использовать генерируемый composer-ом?

                                    может быть переопределено

                                    Мне кажется это не правильно…


                                    1. Rathil
                                      28.05.2015 10:41

                                      то есть, если следовать вашей логикой, вы пишите свои автолоадеры для классов вместо того что бы использовать генерируемый composer-ом?


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


        1. VolCh
          26.05.2015 12:31

          Три причины:
          — отсутствие нормальных автолоадеров для функций
          — логическая принадлежность к классу: те же кастомные конструкторы и прочие фабричные методы
          — исторически сложившаяся привычка, когда нэймспэйсов не было


          1. Ostrovski
            26.05.2015 13:34

            Именно! А справедливое «оправдание» этому — только п.2, т.к. в нем и нужно использовать статические методы.


  1. VolCh
    26.05.2015 12:37
    +2

    90% использования трейтов в моём коде — дефолтная реализация поведенческих интерфейсов типа Observable