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

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'ов.

Расскажите, коллеги, кому приходилось решать похожие задачи, к чему пришли?
 

dark-demon

d(^-^)b
что за странное желание от кого-то наследоваться? о_0"
используй агрегацию и инверсию зависимостей и будет всё в ажуре
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Автор оригинала: dark-demon
используй агрегацию и инверсию зависимостей и будет всё в ажуре
Что такое "агрегация" вроде как понимаю, что такое "инверсия зависимостей" --- не очень, поясни.

-~{}~ 20.09.09 16:39:

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

whirlwind

TDD infected, paranoid
Выдели интерфейс шаблона и примени инверсию зависимостей.

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

Sad Spirit

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

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

whirlwind

TDD infected, paranoid
Внимательнее посмотри, что конкретно у тебя продемонстрировано: 2 класса использующие один и тот же ресурс (соседний класс HTML_QuickForm2_Renderer который $base), различающиеся друг от друга только названием методов.

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

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Автор оригинала: 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()
    {
        // далее по тексту
    }
}

// далее по тексту
Если так уж глаз режет. Ну а дальше-то что?
 

whirlwind

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

-~{}~ 20.09.09 17:50:

ЗЫ. Проблема как раз в том, что доступ у тебя через синглтон. И для того, что бы защить этот единственный экземпляр, тебе нужно так извращаться.
 

Sad Spirit

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

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

whirlwind

TDD infected, paranoid
Все очень просто. На пальцах

Dependency Injection
PHP:
$plugin = new MyPlugin(new Renderer());
Singleton
PHP:
$plugin = new MyPlugin(Renderer::getInstance());
Я через последствия вижу причину ;) Тебе в синглтоне нужны защищенные элементы, которые предназначены для использования другими классами. По нормальному - это называется интерфейс класса. Интерфейс должен быть публичным. Диллема? Нет. Просто подумай, почему интерфейс определенного класса у тебя стремится стать защищенным?
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Чё-то мы ходим кругами.

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

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

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

$plugin = new MyPlugin(Renderer::getInstance());
и постоянное повторение мантры "dependency injection" не отвечает совершенно. Так понятней?
 

whirlwind

TDD infected, paranoid
Ты мешаешь все в кучу, вот и получается паттерн bloat of mud. Потрошки бывают на кухне. А все остальное, к чему код должен получить доступ - это интерфейс. Я дам одну последнюю подсказку: никто не может обращаться к методам и атрибутам конкретного экземпляра который был new, если на него нет референса (здесь выводы насчет protected интерфейса). И наоборот: если любой катях может получить конкретный экземпляр, через getInstance, то защищать его бесполезно (здесь выводы о пользе сингелтонов и об их наследовании).
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Автор оригинала: whirlwind
паттерн bloat of mud... Потрошки бывают на кухне... Я дам одну последнюю подсказку
А вот можно снизойти до простого тупого программиста и сформулировать вот всё тоже самое, только без вы#@онов, но с примером кода? Заранее большое человеческое спасибо.

-~{}~ 20.09.09 18:41:

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

whirlwind

TDD infected, paranoid
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();
    }
    
}
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
О, спасибо огромное! Теперь, когда виден Фасад, наконец всё понятно. :)

Красиво получилось, все поля и методы 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());
    }

    // потрошки
}
 

whirlwind

TDD infected, paranoid
Ну это какбе частности. Главное понять, что декомпозиция она делает программу настоящим конструктором.
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Так, зафиксируем результат. Теперь исходный пример выглядит несколько лучше:
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'а.
 
Сверху