Как стать автором
Обновить

ООП практикум в PHP5: Анализ ошибок, преимуществ и недостатков двух различных реализаций примесей в PHP

Время на прочтение 6 мин
Количество просмотров 2.3K
После выхода моей статьи о примесях мы продолжали дискуссии и в комментариях к статье и в личных сообщениях. Сегодня увидел в моем блоге комментарий от читателя, который попросил объяснить, в чем я вижу преимущество своей реализации перед реализацией Леонида Шлейхера.

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

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

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

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

И я, и Леонид, похоже, допустили одну и ту же ошибку в реализации, связанную со сборкой мусора. Обратите внимание на циклические ссылки: в классе-агрегаторе хранятся ссылки на экземпляры классов примесей, и в примесях, в свою очередь, хранятся ссылки на экземпляры классов агрегаторов. В PHP версиях младше 5.3 произойдет утечка памяти, потому что сборщик мусора не умеет работать с циклическими ссылками. В 5.3 можно активировать специальный сборщик мусора для этой цели, но он замедляет работу скрипта в целом, так что это нежелательно. Что в этом плохого? Все зависит от того, для чего и как используются примеси. Если объектов-агрегаторов со своими примесями много, они большие и сложные, часто создаются / разрушаются – все будет очень плохо. Если же примеси подмешиваются к агрегаторам, существующим на всей протяженности жизни скрипта, ничего страшного не будет, так как вся занимаемая скриптом память при его завершении будет освобождена принудительно.

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

Сейчас вот оказалось, что вариант Леонида вообще не компилируется, потому что магический метод __call() в классе Caller не может быть защищенным. Исправил, заработало. В 5.3 варианте из комментария этой ошибки уже нет.

Леонид реализовал в примеси магические методы, чтобы примесь могла обращаться к членам класса-агрегатора как к своим собственным. Я этого сделать не догадался. Впрочем, я подумаю, реализовывать ли эту идею у себя. Пока что мне не очень нравится граф вызовов, который при этом получается, и некоторая расплывчатость области видимости.

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

Примеси в варианте Леонида реализуются исключительно наследованием. В моем варианте композиция, наряду с наследованием, тоже доступна. Иногда это полезно, если класс невозможно унаследовать от класса-агрегатора.

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

Вариант Леонида реализует не все магические методы у класса-агрегатора (основной акцент сделан на методах), но это легко исправить.

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

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

У Леонида возникает еще одна любопытная ситуация, если в примеси определить защищенный или приватный метод с каким-либо именем, а затем попытаться вызвать метод с этим именем на агрегаторе. Обратите внимание на граф вызовов. У агрегатора вызывается __call(), идет поиск метода с заданным именем среди методов примесей с помощью method_exists(). Совпадение обнаруживается, поскольку method_exists() для скрытых и защищенных членов тоже возвращает true. Производится попытка вызова этого метода уже на экземпляре примеси. Поскольку метод все же защищенный и из данной области видимости недоступен, вызывается уже магический метод примеси __call(), который, в свою очередь, вызывает псевдо-магический метод агрегатора __access_call, в который Леонид забыл добавить проверку ошибок. Поскольку проверка ошибок отсутствует, ничего не происходит и никакой ошибки мы не получаем, хотя мне лично хотелось бы. Да и сам граф сложноват получился, на мой взгляд, для реализации примесей на 80 строк… Ошибку исправить, конечно, легко.

В комментарии к статье автор упоминает, что «каждый родительский класс должен создать экземпляры всех своих примесей. Впрочем, он может использовать общие для всех экземпляры, получая их с помощью Registry». Получать готовые экземпляры в этом случае удобно через реестр только, если примеси не имеют собственного состояния. В противном случае идея теряет смысл и нужно искать другой способ.

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

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

Я активно пользуюсь тайпхинтингом в PHP для дополнительных проверок. У Леонида не заметил, но это пустяки.

В PHP 5.3 варианте на странице комментариев добавлено статическое свойство со списком примесей для статических вызовов. Думаю, выглядит несколько оторвано от контекста, так как на первый взгляд кажется, что примеси делятся на статические и экземплярные, что не соответствует действительности.

Кстати, вариант автора уже работает в проекте, а мой еще нет. Лежит себе на компе без дела…

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

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

Огромная просьба в комментариях не сравнивать две реализации с точки зрения «плохая/хорошая». Уважайте себя. Говоря о достоинствах и недостатках, воздержитесь от демагогии. Факты, факты и еще раз факты. Троллей, обожающих кричать «УГ» и «КГ/АМ» даже не разобравшись в вопросе, просят пройти на посадку, поезд скоро отправится.

Спасибо за внимание.
Теги:
Хабы:
+19
Комментарии 68
Комментарии Комментарии 68

Публикации

Истории

Работа

PHP программист
171 вакансия

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн