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

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

То, что вы изобрели, называется «декоратор» и является стандартным паттерном в программировании. Удачи в изобретении «адептера», «компоновщика», «заместителя», а также других типов шаблонов.
Ненавижу слово «хук», лаконичние использовать observer или «наблюдатель» по нашему…
Не-не-не, не надо мне приписывать чужих подвигов. Я не изобретал ни декораторов, ни CMS, ни ООП вообще. Я пишу о вполне конкретной реализации расширения функционала.

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

$oUser = new DecoratorUser(new ModuleUser());


И сравните с моим вариантом (код условный):

$oUser = E::ModuleUser();
А, вот вы чего сделали. Действительно, это не декоратор.

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

2. Новый человек в проекте такого не поймет и долго будет искать класс PluginFirst_Inherits_ModuleUser. Впрочем, эта проблема решается правильными комментариями.

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

4. Непригодность архитектуры для долгоживущих процессов (невозможно динамически включать и выключать плагины). В этом плане вам несколько повезло с языком программирования, поскольку PHP «рожден умирать». Но всегда попадаются исключения.
По пунктам:

1. Это абсолютно верно. Поскольку речь именно о «динамическом» наследовании, то ни одна IDE этого не поймет. В лучшем случае — это если какое-то специальное расширение для IDE писать (где это возможно). Принимается.

2. Объяснить такие вещи человеку гораздо проще, чем IDE, и грамотная документация решает проблему.

п.3 и п.4 — речь идет о расширение функционала в CMS, написанной на PHP. Само это понятие — CMS на PHP — уже накладывает такое количество ограничений на сферу применения, что эти два пункта, ИМХО, просто тонут в общем потоке.
Здесь есть проблема. Ваш класс ModuleUser — это в чистом виде Public API. Поправьте если не я не прав, но для меня, как для стороннего разработчика плагина, очевидно, что метод Init абстрактный. А это значит что в его реализации я не буду вызывать «parent::Init();», и буду прав. А теперь, мы подключаем сразу 3 плагина, и мой становится последним. И все — инициализация чужих всех остальных плагинов никогда не будет вызвана.
Я так понимаю, что Вы уже глянули, как класс ModuleUser реализован конкретно в Alto CMS? ;)

Если с этой точки зрения смотреть, то да, есть такая проблема. Это «болезнь роста», которая требует решения в последующих версиях.
Ну, это — меньшая из проблем. Если с самого начала понимать, что ты пишешь именно плагин, который может оказаться в любом месте цепочки — то такую ошибку просто не допустишь.
За class_alias большое спасибо, сколько этих функций уже в памяти, а вот такой не припоминаю, полезная.
Хорошо помню, что когда наткнулся на нее в первый раз, то был несколько удивлен: «Нафига?» Т.е. с певого взгляда она мне показалась совершенно бесполезной. Но, как оказалось, на ней целую концепцию выстроить можно :)
У меня сейчас автозагрузчик при загрузке класса cs\Core проверяет есть ли cs\custom\Core и так далее. Но это не позволяет сделать множественного переопределение функциональности. Но сохраняется работоспособность автодополнения в любой IDE, так как вы всё ещё вызываете cs\Core::instance().
А с class_alias() можно придумать что-то ещё более веселое и менее ограниченное) При этом хочется обойтись без eval()
Не понимаю. Какой смысл использовать наследование для задачи которая решается композицией? Наследование это зло в большинстве случаев.
В целом очень сложное решение. Явная нехватка явности и гибкости. Думаю, все-таки система событий и слушателей здесь подошла бы намного лучше.
А если попробовать абстрагироваться от тезиса, что «наследование это зло» (сегодня я не готов к халивару «композиция vs наследование»), то в чем конкретно ощущается нехватка явности и гибкости?

Ведь по сути своей «хуки» — это та же система «событий и слушателей», и вот ее мне было однозначно мало. Мне, например, не хватало того, что код, исполняемый во время вызова, был жестко изолирован от той среды, в которой выполнялся весь остальной код класса. А здесь — все нативно и органично. Т.е. для разработчика, пишущего расширение — ООП в самом чистом виде без всяких извращений.
Не совсем понимаю, как сам по себе подход с событиями связан с проблемами изоляции. В любом подходе можно сделать любую степень изоляции, но используя наследование ты создаешь проблемы с которыми сам будешь бороться. В целом я не хочу сказать, что наследование вообще неприменимо и уж тем более холиварить на эту на эту тему, но именно в этом случае это не дает ничего кроме проблем. вроде вызова перентовой реализации и неявности вроде наследования от какого-то непонятного класса.
Возможно, проблемы с изоляцией это вопрос инкапсуляции. Вроде отсутствия хорошо продуманных интерфейсов для работы с системами цмс из плагинов и как следствие необходимость работать с внутренним состоянием классов.
Система событий и слушателей рискует выродиться в систему костылей и подпорок — я видел кучу проектов, где так и произошло. А автору статьи подошел бы в чистом виде паттерн «декоратор» — он очень похож на решение автора, но при этом куда проще реализуется.
Да собственно, те самые «хуки» — это и есть система «событий и слушателей», которая активно использовалась в PHP-проектах еще до прихода туда нормального ООП, и, наверное, любая более-менее популярная CMS их использует. Но, как Вы верно отметили, нередко это вырождается в жуткую систему костылей и подпорок.

Да и вообще, ни один паттерн не гарантирован от «грязной» реализации, к сожалению.
А что произойдет, если в модуле PluginFirst будет объявлен вспомогательный метод HandleStuff и член класса $someData, а в модуле PluginSecond будет объявлен другой метод HandleStuff, и в модуле PluginThird будет объявлен (и будет вовсю использоваться) член класса $someData? Коллизия имен и вызов неверных методов с неверными параметрами? Непонятное изменение члена класса с тяжелой отладкой? А что, если $someData был сначала объявлен как public:, а потом был объявлен как private:?
Т.к. тут все строится на ООП, то подразумевается, что разработчики, использующие данный подход, понимают, что это такое и как работает. Объявление любого метода или свойства как public или protected означает, что эти методы и свойства доступны в классах-наследниках. И если не задумываться об этом, то да, вполне может возникнуть коллизия. Но ровно это же может случиться, если не касаться описанных в статье подходов, а просто, работая в каком-то проекте на ООП, расширять цепочку классов, создавая свой дочерний класс.
Поправка, тут от ООП только наследование… и все. Ни нормальной инкапсуляции, ни вообще ничего…
«Работая в каком-то проекте и создавая свой дочерний класс», такая ошибка будет отловлена в два счета, на этапе разработки.
А вот ваша реализация породит кучу «Гейзенбагов», которые отлавливаются только при каком-то определенном наборе модулей, когда любой из модулей работает правильно, но их одновременное включение порождает баги.
Хочу акцентировать внимание: речь идет о CMS, и о возможности разрабатывать плагины, расширяющие любую сущность движка, независимыми друг от друга разработчиками.

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


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

Вообще наследование зло, особенно в этом контексте. Нет возможности задавать приоритеты, какие-то вещи скажем должны быть последними в цепочке а какие-то первыми а какие-то последними (кеширование в цепочке должно быть последним скажем, а замена доступа к хранилищу данных первым звеном).

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

Грусть тоска по неймспейсам, PSR-4 и вообще всему тому что PHP комьюнити наработало за последние пару тройку лет…
Извините за сумбур и оговорки, вечер понедельника сказывается.
vshemarov
И как только будет обращение к классу ModuleUser (создание экземпляра объекта выполняется через вызов специального метода), то… начинается «магия».
Как я понимаю — для того чтобы все работало нельзя создавать объект как обычно:
$object = new ModuleUser();
а надо использовать некий специальный метод.
Могли бы вы немного подробнее описать и показать — как конкретно это будет выглядеть в вашем коде?
Выше в комментах я приводил пример, как может выглядеть метод для создания объекта:

$object = E::ModuleUser();

Где E — это класс (singleton), выполняющий функции ядра, и там есть __callStatic, который получает управление и понимает, что нужно создать объект класса ModuleUser. И вот он уже смотрит стек наследования, определяет, нужно ли непосредственно класс ModuleUser брать (если наследников нет), либо кого-то, кто его переопределяет.
vshemarov
Где E — это класс (singleton), выполняющий функции ядра, и там есть __callStatic, который получает управление и понимает, что нужно создать объект класса ModuleUser
Ясно. Но почему вы не хотите использовать для этого отдельную функцию? Например:

$object = newObject('ModuleUser');

чтобы хоть немного «помочь» IDE и разработчику. Вы конечно можете возразить, что в вашем случае есть возможность передать в конструктор аргументы, типа:

$object = E::ModuleUser($arg1, $arg2);

но, при вызове __callStatic все равно все аргументы будут «собраны» в массив $arguments:

public static function __callStatic($name, $arguments) { ... }

и если я не ошибаюсь, то вы не сможете использовать их явно в конструкторе плагина:

class PluginFirst_ModuleUser extends PluginFirst_Inherits_ModuleUser
{
    public function __construct($arg1, $arg2)
    {
        parent::__construct($arg1, $arg2);
        // New code here
    } 
}

все равно придется использовать func_get_args в этих конструкторах. Я думаю, что использование отдельной функции для создания объекта с динамическим автонаследованием классов вместо __callStatic будет «понятнее» и IDE и разработчику, например:

$object = newObject('ModuleUser', array($arg1, $arg2));
Вообще говоря, если действовать в духе статьи, то проще всего сделать алиас ModuleUser_AllPlugins, указывающий на конец цепочки, и вызывать конструктор напрямую…
Ну, это опять же уже детали реализации. Развивая мысль, можно, при желании, и как-то так сделать:

$object = new InstanceOfModuleUser($arg1, $arg2);

Но это, опять же, кому-то понравится, кому-то нет, дело вкуса
Вы написали абсолютно то же самое, что и я.
Я слегка видоизменю код, чтоб уровнять визуально:
$object = E::newObject('ModuleUser', array($arg1, $arg2));

$object = E::newModuleUser($arg1, $arg2);

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

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

Зато во втором случае можно хоть небольшую, но дать подсказку IDE:
/**
 * @method ModuleUser ModuleUser()
 */
class E {
    // ...
}

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

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

Но если Вам действительно интересно, то плагины есть как на гитхабе, так и в каталоге (в каталоге их больше, чем на гитхабе). Но чтоб долго не искать, вот, например, три плагина для Alto CMS с гитхаба:
github.com/altocms/alto-plugin-sitemap
github.com/altocms/alto-plugin-topicintro
github.com/altocms/alto-plugin-similartopics

И они, кроме прочего, расширяют много компонентов, но каждый из них расширяет сущность Topic (или «коробочный» класс ModuleTopic_EntityTopic движка). Чтоб долго не ковыряться в коде, могу привести ссылки конкретно на классы-расширения:
github.com/altocms/alto-plugin-sitemap/blob/master/classes/modules/topic/entity/Topic.entity.class.php
github.com/altocms/alto-plugin-topicintro/blob/master/classes/modules/topic/entity/Topic.entity.class.php
github.com/altocms/alto-plugin-similartopics/blob/master/classes/modules/topic/entity/Topic.entity.class.php

Я правильно понял, Вы это хотели увидеть?
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории