Реализация шаблона Visitor

Опубликовал read-php в 11.02.2011 Категория: Выполнение задач и представление результатов в PHP

Давайте начнем с наших интерфейсов. В абстрактном классе Unit определим метод accept ().

function accept( ArmyVisitor $visitor ) {

$method = «visit» . get_class( $this );

$visitor->$method( $this );

}

protected function setDepth( $depth ) {

$thi s->depth= $depth;

}

function getDepthO { return $this->depth;

}

Как видите, метод accept () ожидает, что ему будет передан объект ArmyVisitor. В РНР можно динамически определить в объекте ArmyVisitor метод, который мы хотим вызвать. Это позволит нам не реализовывать метод accept () на каждом листовом узле в нашей иерархии классов. Для удобства я также добавил два метода getDepth () и setDepth (). Их можно использовать, чтобы сохранять и извлекать значение вложенности элемента на дереве. Метод setDepth () вызывается родителем элемента, когда тот добавляет его к дереву из метода CompositeUnit: :addUnit ().

function addUnit( Unit $unit ) {

foreach ( $this->units as $thisunit ) {

if ( $unit === $thisunit ) {

return;

}

}

$unit->setDepth ($this- >depth+l) ; $this->units[] = $unit;

}

И нам нужно определить только еще один метод accept () в абстрактном классе-композите.

function accept( ArmyVisitor $visitor ) {

$method = «visit» . get_class( $this );

$visitor->$method( $this ) ;

foreach ( $this->units as $thisunit ) {

$thisunit->accept{ $visitor );

}

}

Этот метод делает то же самое, что и unit:: accept (), но с одним добавлением. Он динамически создает имя метода на основе имени текущего класса и вызывает
этот метод для предоставленного объекта ArmyVisitor. Поэтому, если текущим классом является Army, он вызывает ArmyVisitor: :visitArmy(), а если текущий класс — TroopCarr ier, то он вызывает ArmyVisitor:: vis itTroopCarr ier (), и т.д. Сделав это, он затем проходит по циклу все дочерние объекты, вызывая метод accept (). На самом деле, поскольку метод accept () заменяет операцию своего родителя, мы можем здесь избавиться от повторения.

function accept( ArmyVisitor $visitor ) {

parent::accept( $visitor );

foreach ( $this->units as $thisunit ) { $thisunit-»accept( $visitor );

}

}

Устранение повторения таким способом может быть очень удачным, но в данном случае мы избавились только от одной строки, возможно, немного потеряв в ясности. Но в любом случае метод accept () позволяет делать следующее:

  • вызывать корректный метод посетителя для текущего компонента;
  • передавать объект-посетитель всем текущим дочерним элементам с помощью метода accept () (предполагая, что текущий компонент—это композит).

Мы должны еще определить интерфейс для ArmyVisitor. Для этого некоторую информацию должны дать методы accept (). Класс-посетитель должен определить методы accept () для каждого из конкретных классов в иерархии классов. Это позволит обеспечить разные функции для разных объектов. В моей версии данного класса я также определил стандартный метод visit (), который автоматически вызывается, если реализующие классы «решают» не выполнять специальную обработку для определенных классов Unit.

abstract class ArmyVisitor {

abstract function visit( Unit $node );

function visitArcher( Archer $node ) { $this-»visit( $node );

}

function visitCavalry( Cavalry $node ) { $this-»visit( $node );

}

function visitLaserCannonUnit( LaserCannonUnit $node ) { $this-»visit ( $node )

}

function visitTroopCarrierUnit( TroopCarrierUnit $node ) { $this-»visit( $node );

}

function visitArmy( Army $node ) { $this-»visit( $node );

}

}

Теперь остается только вопрос предоставления реализаций класса ArmyVisitor, и мы готовы к работе. Вот простой пример кода, выводящего текстовый дамп, заново реализованный на основе объекта Armyvisitor.

class TextDumpArmyVisitor extends ArmyVisitor {

private $text=»";

function visit( Unit $node ) { $ret = «»;

$pad = 4*$node->getDepth()

$ret .= sprintf( «%{$pad}s», «» );

$ret .= get_class($node).»: «;

$ret = «Огневая мощь: » . $node->bombardStrength() . n\n»; $this->text .= $ret;

}

function getText() {

return $this->text;

}

}

Давайте рассмотрим клиентский код, а затем пройдемся по всему процессу.

$main_army = new Army ();

$main_army->addUnit( new Archer() );

$main_army->addUnit( new LaserCannonUnit() );

$main_army->addUnit( new Cavalry() );

$textdump = new TextDumpArmyVisitor();

$main_army->accept( $textdump ); print $textdump->getText();

Этот код на выходе даст следующее.

Army: Огневая мощь: 50

Archer: Огневая мощь: 4

LaserCannonUnit: Огневая мощь: 44

Cavalry: Огневая мощь: 2

Мы создаем объект Army. Поскольку Army — композит, у него есть метод addUnit (), который мы используем для добавления дополнительных объектов типа Unit. Затем мы создаем объект TextDumpArmyVisitor. Мы передаем его методу Army:: accept (). Метод accept () на ходу создает имя метода и вызывает TextDumpArmyVisitor: : visitArmy (). В данном случае мы не обеспечили специальной обработки для объектов типа Army, поэтому вызов передается общему методу visit (). Методу visit () передается по ссылке наш объект Army. Он вызывает свои методы (включая вновь добавленный getDepth (), который сообщает всем, кому нужно знать, глубину вложения элемента в иерархии объекта), чтобы сгенерировать итоговые данные. Вызов visitArmy () выполнен, операция Army: : accept () теперь вызывает по очереди метод accept () для своих дочерних объектов, передавая объект-посетителя. В результате класс ArmyVisitor посетит каждый объект на дереве.

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

На некоторых клетках в нашей игре армии должны платить налоги. Сборщик налогов посещает армию и берет плату за каждый элемент (подразделение), который он находит. Разные подразделения должны платить разные суммы налогов. Здесь мы можем воспользоваться преимуществами специализированных методов в классе-посетителе.

class TaxCollectionVisitor extends ArmyVisitor {

private $due=0;

private $report=»";

function visit( Unit $node ) {

$this->levy( $node, 1 );

}

function visitArcher( Archer $node ) {

$this->levy( $node, 2 );

}

function visitCavalry( Cavalry $node ) {

$this->levy( $node, 3 );

}

function visitLaserCannonUnit( LaserCannonUnit $node ) {

$this-»levy{ $node, 5 );

}

private function levy( Unit $unit, $amount ) {

$thie->report .= «Налог для » . get_class ( $unit );

$this->report . = «: $amount\n»;

$this->due += $amount;

}

function getReportO { return $this-»report;

}

function getTaxO { return $this-»due;

}

}

В этом простом примере мы непосредственно не используем объекты Unit, переданные различным методам visit. Но мы используем специализированную природу этих методов, взимая различные суммы налогов в соответствии с конкретным типом вызывающего объекта Unit. Вот пример клиентского кода.

$main_army = new Army() ;

$main_army->addUnit( new Archer() );

$main_army->addUnit( new LaserCannonUnit() ); $main_army->addUnit( new Cavalry() );

$taxcollector = new TaxCollectionVisitor();

$main_army->accept{ $taxcollector ) ;

print $taxcollector-»getReport() . «\n»;

print «ИТОГО: «;

print $taxcollector-»getTaxO . «\n»;

Объект TaxCollectionVisitor передается методу accept () объекта Army, как и раньше. И снова Army передает ссылку на себя методу visitArmy(), прежде чем вызывать метод accept () для своих дочерних объектов. Эти компоненты понятия не имеют, какие операции выполняет их посетитель. Они просто сотрудничают с их общедоступным интерфейсом, причем каждый послушно передает ссылку на себя методу, соответствующему его типу.

В дополнение к методам, определенным в классе ArmyVisitor, класс TaxCollectionVisitor предоставляет два итоговых метода, getReport () и getTaxO. Вызов этих методов предоставляет данные, которые мы и ожидали получить.

Налог для Army: 1

Налог для Archer: 2

Налог для LaserCannonUnit: 5

Налог для Cavalry: 3

ИТОГО: 11

На рис. показаны объекты, участвующие в этом примере.

Программирование PHP 19 Реализация шаблона Visitor

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

Добавить комментарий