добавление методов класса / объекта в процессе работы

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
добавление методов класса / объекта в процессе работы

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

Откуда вообще возник вопрос: вывод формы в предыдущей версии пакета осуществлялся классами-Renderer'ами, реализующими паттерн Visitor. В принципе всё это жужжало весьма неплохо, позволяя выводить форму посредством кучи не очень между собой похожих шаблонных движков. Но: не было простого способа расширить функциональность этих самых Renderer'ов, поэтому товарищи, писавшие всякие сложные дополнительные элементы, фактически писали для них свой вывод с шаблонами, блекджеком и шлюхами.

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

Почему нельзя обойтись наследованием? Пусть у нас есть два дополнительных элемента HTML_QuickForm2_Element_Foo и HTML_QuickForm2_Element_Bar, распространяющихся в отдельных пакетах, при этом каждому из них нужен какой-то специальный вывод. Ну делаем наследование
Код:
HTML_QuickForm2_Renderer_Default
  |
  +-- HTML_QuickForm2_Renderer_Default_Foo
  \-- HTML_QuickForm2_Renderer_Default_Bar
и садимся в лужу, поскольку объединить их функциональность в одном классе, экземпляр которого нам надо передавать, будет трудновато --- в похапэ нет множественного наследования.

Соответственно надо как-то реализовать систему Plugin'ов, которая позволит добавлять методы на ходу. Теоретически в похапэ 5.3 есть замыкания, но в них нельзя использовать $this и обращаться к защищённым методам и атрибутам. Теоретически также есть расширение runkit, но оно таки нестандартное и сильно уменьшит аудиторию пакета. Остаётся вариант с __call().

Ну и ещё закономерное требование, чтобы была возможность отложенной инициализации: Renderer'ов может быть много, на каждый по plugin'у, а реально использоваться будет, как правило, один. И при этом хотелось бы, чтобы при регистрации нового plugin'а он магически добавлялся в существующие экземпляры класса Renderer'а.

В общем, приняв во внимание все требования, в результате пришёл к следующему дизайну базового класса. Работает оно примерно так:
PHP:
require_once 'HTML/QuickForm2/Renderer/Default.php';
require_once 'HTML/QuickForm2/Renderer/Plugin.php';

// Приходится наследовать, так как иначе до protected атрибутов и методов не добраться
class HTML_QuickForm2_Renderer_Default_HelloPlugin
    extends HTML_QuickForm2_Renderer_Default
    implements HTML_QuickForm2_Renderer_Plugin
{
    protected $base;

    public function setRenderer(HTML_QuickForm2_Renderer $base)
    {
        $this->base = $base;
    }

    public function sayHello()
    {
        $elTpl = $this->base->markRequired(
                    $this->base->outputError(
                        $this->base->outputLabel(
                            $this->base->templatesForClass['html_quickform2_element'],
                            __CLASS__ . ' says:'
                        ), ''
                    ), false
                 );
        echo str_replace(array('{id}', '{element}'), array('', 'Hello, world!'), $elTpl);
    }
}

class HTML_QuickForm2_Renderer_Default_GoodbyePlugin
    extends HTML_QuickForm2_Renderer_Default
    implements HTML_QuickForm2_Renderer_Plugin
{
    protected $base;

    public function setRenderer(HTML_QuickForm2_Renderer $base)
    {
        $this->base = $base;
    }

    public function sayGoodbye()
    {
        $elTpl = $this->base->markRequired(
                    $this->base->outputError(
                        $this->base->outputLabel(
                            $this->base->templatesForClass['html_quickform2_element'],
                            __CLASS__ . ' says:'
                        ), ''
                    ), false
                 );
        echo str_replace(array('{id}', '{element}'), array('', 'Goodbye, world!'), $elTpl);
    }
}

HTML_QuickForm2_Renderer::registerPlugin('default', 'HTML_QuickForm2_Renderer_Default_HelloPlugin');
$renderer = HTML_QuickForm2_Renderer::getInstance('default');
// магически добавится к экземпляру класса
HTML_QuickForm2_Renderer::registerPlugin('default', 'HTML_QuickForm2_Renderer_Default_GoodbyePlugin');

$renderer->sayHello();
$renderer->sayGoodbye();

Эстетическое чувство, впрочем, не совсем успокоилось:
  • Экземпляры классов Renderer'ов --- Singleton'ы. Тут вот как раз срач на эту тему идёт. Неудобно тестировать, приходится заводить методы для сбрасывания состояния.
  • Наследование плагинов от Renderer'ов.

Расскажите, коллеги, кому приходилось решать похожие задачи, к чему пришли?
 
что за странное желание от кого-то наследоваться? о_0"
используй агрегацию и инверсию зависимостей и будет всё в ажуре
 
Автор оригинала: dark-demon
используй агрегацию и инверсию зависимостей и будет всё в ажуре
Что такое "агрегация" вроде как понимаю, что такое "инверсия зависимостей" --- не очень, поясни.

-~{}~ 20.09.09 16:39:

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

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

PS. На самом деле, у тебя и так это уже сделано. Непонятно только почему общие методы (инжект шаблона) у тебя дублируется. И что же там на самом деле такого нужного в базовых классах то же неизвестно.
Общие методы == setRenderer()? Потому что он определён в интерфейсе, вестимо, а не в классе. А в интерфейсе он потому, что мы наследуем конкретный Plugin от конкретной реализации Renderer'а, а множественного наследования в пых-пыхе нету. А наследуем мы от конкретной реализации потому, что нам нужен доступ к protected методам и полям, а в пых-пыхе protected только в дереве наследования доступен. Ну а доступ нужен потому, что там всякие полезные вспомогательные методы и не менее полезные поля. В примере ж оно продемонстрировано.
 
Внимательнее посмотри, что конкретно у тебя продемонстрировано: 2 класса использующие один и тот же ресурс (соседний класс HTML_QuickForm2_Renderer который $base), различающиеся друг от друга только названием методов.

Я не вижу ничего общего, кроме protected $base, который по моей логике должен быть в базовом классе. Что же реально у тебя в базовом классе, для меня загадка, потому что из примера этого не видно. И увидеть здесь необходимость такого наследования я не могу. Я делаю выводы что это не нужно, либо пример неудачный.
 
Автор оригинала: whirlwind
Внимательнее посмотри, что конкретно у тебя продемонстрировано: 2 класса использующие один и тот же ресурс (соседний класс HTML_QuickForm2_Renderer который $base), различающиеся друг от друга только названием методов.

Я не вижу ничего общего, кроме protected $base, который по моей логике должен быть в базовом классе. Что же реально у тебя в базовом классе, для меня загадка, потому что из примера этого не видно. И увидеть здесь необходимость такого наследования я не могу. Я делаю выводы что это не нужно, либо пример неудачный.
Они на самом деле используют HTML_QuickForm2_Renderer_Default, от которого и унаследованы. В этом классе реализован вывод формы посредством простеньких шаблонов, и эти классы используют хранящиеся там в полях шаблоны и методы, которые объявлены protected.

Далее, 2 этих класса, которые здесь рядом, будут на самом деле находиться в двух разных пакетах, будут написаны разными людьми, и друг о друге знать не будут ничего. Хорошо, объявим
PHP:
class HTML_QuickForm2_Renderer_Default_Plugin
    extends HTML_QuickForm2_Renderer_Default
    implements HTML_QuickForm2_Renderer_Plugin
{
    protected $base;

    public function setRenderer(HTML_QuickForm2_Renderer $base)
    {
        $this->base = $base;
    }
}

class HTML_QuickForm2_Renderer_Default_HelloPlugin
    extends HTML_QuickForm2_Renderer_Default_Plugin
{
    public function sayHello()
    {
        // далее по тексту
    }
}

class HTML_QuickForm2_Renderer_Default_GoodbyePlugin
    extends HTML_QuickForm2_Renderer_Default_Plugin
{
    public function sayGoodbye()
    {
        // далее по тексту
    }
}

// далее по тексту

Если так уж глаз режет. Ну а дальше-то что?
 
Ты имеешь в виду, что такой подход исключительно из-за $this->base->templatesForClass ? И ты не видешь более изящного выхода из ситуации?

-~{}~ 20.09.09 17:50:

ЗЫ. Проблема как раз в том, что доступ у тебя через синглтон. И для того, что бы защить этот единственный экземпляр, тебе нужно так извращаться.
 
Автор оригинала: whirlwind
Ты имеешь в виду, что такой подход исключительно из-за $this->base->templatesForClass ? И ты не видешь более изящного выхода из ситуации?
...а также из-за this->base->markRequired(), $this->base->outputError() и $this->base->outputLabel(). Пропихнуть protected поля в класс-plugin как раз проще, чем использовать из него protected методы.

ЗЫ. Проблема как раз в том, что доступ у тебя через синглтон. И для того, что бы защить этот единственный экземпляр, тебе нужно так извращаться.
Синглтон-то тут при чём?.. Ну и в принципе, от него можно избавиться, переделав, допустим, метод __call().
 
Все очень просто. На пальцах

Dependency Injection
PHP:
$plugin = new MyPlugin(new Renderer());

Singleton
PHP:
$plugin = new MyPlugin(Renderer::getInstance());

Я через последствия вижу причину ;) Тебе в синглтоне нужны защищенные элементы, которые предназначены для использования другими классами. По нормальному - это называется интерфейс класса. Интерфейс должен быть публичным. Диллема? Нет. Просто подумай, почему интерфейс определенного класса у тебя стремится стать защищенным?
 
Чё-то мы ходим кругами.

Ещё раз: есть класс Renderer, у которого есть вполне себе публичный интерфейс, состоящий из методов типа render*(). У него также есть потрошки, которые совершенно не обязаны быть публичными, там разные вспомогательные методы, неинтересные для вызова снаружи и поля, хранящие внутреннее состояние.

При этом
а) нужно иметь возможность расширить этот публичный интерфейс новыми методами;
бэ) то, чем мы его расширяем, должно иметь доступ к потрошкам;
вэ) по возможности, вся эта хрень должна поддерживать lazy initialization: нам не нужно 5 renderer'ов с 5-ю плагинами каждый, нам обычно нужен один со своими 5-ю плагинами.

Синглтон, в данном случае, имеет отношение только к реализации пункта вэ), если его выкинуть, пункты а) и бэ) останутся. Как их реализовать показ мне "на пальцах"
PHP:
$plugin = new MyPlugin(new Renderer());

$plugin = new MyPlugin(Renderer::getInstance());
и постоянное повторение мантры "dependency injection" не отвечает совершенно. Так понятней?
 
Ты мешаешь все в кучу, вот и получается паттерн bloat of mud. Потрошки бывают на кухне. А все остальное, к чему код должен получить доступ - это интерфейс. Я дам одну последнюю подсказку: никто не может обращаться к методам и атрибутам конкретного экземпляра который был new, если на него нет референса (здесь выводы насчет protected интерфейса). И наоборот: если любой катях может получить конкретный экземпляр, через getInstance, то защищать его бесполезно (здесь выводы о пользе сингелтонов и об их наследовании).
 
Автор оригинала: whirlwind
паттерн bloat of mud... Потрошки бывают на кухне... Я дам одну последнюю подсказку
А вот можно снизойти до простого тупого программиста и сформулировать вот всё тоже самое, только без вы#@онов, но с примером кода? Заранее большое человеческое спасибо.

-~{}~ 20.09.09 18:41:

Нет, то есть мысль о том, что надо никому не показывать экземпляр Renderer'а, она, конечно новая и интересная. Но: к сожалению экземпляр Renderer'а может быть использован сам по себе, безо всяких плагинов. А когда будут плагины, то их может быть несколько. Так что всё-таки хотелось бы код посмотреть.
 
PHP:
class Renderer {
   // potroshki (c) here
}

interface IPlugin {
    public function run();
}

abstract class AbstractPlugin implements IPlugin {
    protected $renderer;
    
    function construct($r)
    {
        $this->renderer = $r;
    }
    
}

class Plugin_sayGoodbye extends AbstractPlugin {

    function run()
    {
        $elTpl = $this->renderer->markRequired(
                    $this->renderer->outputError(
                        $this->renderer->outputLabel(
                            // тут бы конечно метод сделать, но и public атрибут то же сработает
                            $this->renderer->templatesForClass['html_quickform2_element'],
                            __CLASS__ . ' says:'
                        ), ''
                    ), false
                 );
        echo str_replace(array('{id}', '{element}'), array('', 'Goodbye, world!'), $elTpl);
    }
}

class PluginFactory {

    function produce($pluginName, $args)
    {
        // load class
        $className = 'Plugin_' . $pluginName;
        return new $className($args);
    }

}

class CachingFactory // eg registry + factory
{

}

class QuickForm2_Facade
{
    // метода getRenderer тут нету

    private function __construct($renderer, $factory)
    {
        //
    }
    
    function getInstance()
    {
        return new QuickForm2_Facade(new Renderer(),
                                     new CachingFactory(new PluginFactory()));
    }
    
    function __call($method, $args)
    {
        $this->factory->produce($method, $this->renderer)->run();
    }
    
}
 
О, спасибо огромное! Теперь, когда виден Фасад, наконец всё понятно. :)

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

Я бы только вот так переработал, чтобы Renderer ещё и создать напрямую нельзя было:
PHP:
class Renderer_Facade
{
    protected $renderer;

    public function __construct(Renderer $renderer, $factory)
    {
        $this->renderer = $renderer;
    }

    public function __call($method, $args)
    {
        if (method_exists($this->renderer, $method)) {
            return call_user_func_array(array($this->renderer, $method), $args);
        }
        return $this->factory->produce($method, $this->renderer)->run();
    }
}

class Renderer
{
    protected __construct()
    {
    }

    public function getInstance($type)
    {
         $className = 'Renderer_' . $type;
         return new Renderer_Facade(new $className, new PluginFactory());
    }

    // потрошки
}
 
Ну это какбе частности. Главное понять, что декомпозиция она делает программу настоящим конструктором.
 
Так, зафиксируем результат. Теперь исходный пример выглядит несколько лучше:
PHP:
require_once 'HTML/QuickForm2/Renderer.php';
require_once 'HTML/QuickForm2/Renderer/Plugin.php';

// Из plugin'а доступ к атрибутам и методам Renderer'а есть, ибо они публичные.
// Снаружи доступа нету, т.к. не добраться до экземпляра класса
class HTML_QuickForm2_Renderer_Default_HelloPlugin
    extends HTML_QuickForm2_Renderer_Plugin
{
    public function sayHello()
    {
        $elTpl = $this->renderer->markRequired(
                    $this->renderer->outputError(
                        $this->renderer->outputLabel(
                            $this->renderer->templatesForClass['html_quickform2_element'],
                            __CLASS__ . ' says:'
                        ), ''
                    ), false
                 );
        echo str_replace(array('{id}', '{element}'), array('', 'Hello, world!'), $elTpl);
    }
}

class HTML_QuickForm2_Renderer_Default_GoodbyePlugin
    extends HTML_QuickForm2_Renderer_Plugin
{
    public function sayGoodbye()
    {
        $elTpl = $this->renderer->markRequired(
                    $this->renderer->outputError(
                        $this->renderer->outputLabel(
                            $this->renderer->templatesForClass['html_quickform2_element'],
                            __CLASS__ . ' says:'
                        ), ''
                    ), false
                 );
        echo str_replace(array('{id}', '{element}'), array('', 'Goodbye, world!'), $elTpl);
    }
}

HTML_QuickForm2_Renderer::registerPlugin('default', 'HTML_QuickForm2_Renderer_Default_HelloPlugin');

$renderer = HTML_QuickForm2_Renderer::factory('default');

// магически добавится к экземпляру класса
HTML_QuickForm2_Renderer::registerPlugin('default', 'HTML_QuickForm2_Renderer_Default_GoodbyePlugin');

$renderer->sayHello();
$renderer->sayGoodbye();

Ну и соответственно текущие версии Renderer'а, Proxy, Plugin'а.
 
Сверху