11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHPで使うGoFパターン ひとり Advent Calendar - ビジター

Last updated at Posted at 2012-08-16

ビジターってなに?

要約するとデータ構造とアルゴリズムを分離した設計を行うことです。オブジェクト指向に対する反逆ですね。ですが、上手く使えばクラス間の結合度を下げることが出来るのでユニットテストがし易くなる安全なパターンとも言えるかも知れません。後述のダブルディスパッチという特徴が分かり難いかも知れませんが。

ビジターの構造

アクセプター - データです
ビジター - アルゴリズムです

ダブルディスパッチ

アルゴリズムとデータ構造を分ける…… と聞いた時に、アルゴリズムがデータ構造を入力で受け取って処理するのだろうなーと考えたのではないでしょうか。構造体と関数の関係であればそうなのですが、このパターンは斜め上を行きます。アルゴリズムがデータ構造を訪れる(visit)のです。でも、それでは何がダブルなのかイマイチですね。実際にコードを見てみましょう。

<?php
function say ( $s ) { print "$s\n"; }
interface IVisitor {
  public function visit ( $acceptor );
}

interface IAcceptor {
  public function accept ( $visitor );
}

//
// MEMO:ここまで見てエエーッ…… と思った方は勘が良いです
//

class Visitor implements IVisitor {
  public function visit ( $acceptor ) {
    if ( !( $acceptor instanceof IAcceptor ) ) 
      throw new Exception ( '無理だもん' );
    $method = 'visit' . get_class ( $acceptor );
    if ( !method_exists ( $this, $method ) ) 
      throw new Exception ( 'そんなメソッド無いもん' );
    // オーバーロードが無いんだもん
    $this->$method ( $acceptor );
  }

  public function visitAcceptor1 ( $acceptor ) {
    //
    // Acceptor1に関する処理をします
    //
    say ( 'Acceptor1に処理してるよ' );

    //  Acceptor1に紐付く Acceptorを訪れます
    $acceptor->Acceptor1_1->accept ( $this );
  }

  public function visitAcceptor1_1 ( $acceptor ) {
    say ( 'Acceptor1_1に処理してるよ' );
  }

  public function visitAcceptor2 ( $acceptor ) {
    say ( 'Acceptor2に処理してるよ' );
    $acceptor->Acceptor2_1->accept ( $this );
  }

  public function visitAcceptor2_1 ( $acceptor ) {
    say ( 'Acceptor2_1に処理してるよ' );
  }
}

class Acceptor1 implements IAcceptor {
  private $_Values = array ();
  public function __get ( $key )       { return $this->_Values[$key]; }
  public function __set ( $key, $val ) { $this->_Values[$key] = $val; }
  public function accept ( $visitor )  { $visitor->visit ( $this ); }
}

class Acceptor1_1 implements IAcceptor {
  private $_Values = array ();
  public function __get ( $key )       { return $this->_Values[$key]; }
  public function __set ( $key, $val ) { $this->_Values[$key] = $val; }
  public function accept ( $visitor )  { $visitor->visit ( $this ); }
}

class Acceptor2 implements IAcceptor {
  private $_Values = array ();
  public function __get ( $key )       { return $this->_Values[$key]; }
  public function __set ( $key, $val ) { $this->_Values[$key] = $val; }
  public function accept ( $visitor )  { $visitor->visit ( $this ); }
}

class Acceptor2_1 implements IAcceptor {
  private $_Values = array ();
  public function __get ( $key )       { return $this->_Values[$key]; }
  public function __set ( $key, $val ) { $this->_Values[$key] = $val; }
  public function accept ( $visitor )  { $visitor->visit ( $this ); }
}

$obj1 = new Acceptor1 ();
$obj2 = new Acceptor2 ();
$obj1->Acceptor1_1 = new Acceptor1_1();
$obj2->Acceptor2_1 = new Acceptor2_1();
$obj1->accept ( new Visitor () );
$obj2->accept ( new Visitor () );

どういう時に使うの?

メリットとしてはデータ構造の変更があっても、アルゴリズム側が影響受け難いところでしょうか。しかし、明確なオーバーロードの無いPHPだとvisitの入口が無駄に複雑になりますので実装が微妙です。しかも、よく考えて使わないとVisitorがどんどん太っていって、手を付けられなくなります。Visitorは「ひとつの一連の操作」につきひとつ用意します。つまり、ひとつのデータ構造に対してVisitorは複数居るような状態ですね。向いている操作としてはツリー構造を遡って一定のルールで操作をするような時です。「ツリー構造を遡りながら影響与えたい箇所だけ処理をする」という一連の流れがいっぱい有る時という用途です。

具体的にはディレクトリ構造を遡りながら特定のパターンにマッチしたファイルを削除するようなVisitorパターンが向いています。しかし、その例のような完全なトラバースをするのであればイテレータパターンとCoRの組み合わせが、より向いているかも知れません。さらに言うと、どうしてもオブジェクト指向に拘る必要が無いのならば再起と配列で書いた方が分かり易いケースの方が多いかも知れません。一方でユニットテストのし易さ、ユニットテストによる問題の発見のし易さは、こちらが上回るのでご利用は計画的に。

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?