1. Home
  2. PHP
  3. DesignPatterns
  4. Visitorパターン - PHPデザインパターン

Visitorパターン - PHPデザインパターン

  • 公開日
  • 更新日
  • カテゴリ:DesignPatterns
  • タグ:PHP,Behavior,DesignPatterns,Visitor
Visitorパターン - PHPデザインパターン

Visitor パターンは、振る舞いに関するデザインパターン手法で、機能(クラス)の持つ役割を分離する事でそれぞれの利用・機能拡張を容易にする処理モデルです。

Visitor パターンのクラス図は以下の通りです。

実装例

今回は、ファイルの階層、あるディレクトリとファイルの範囲の中で、そこに存在するディレクトリの数とファイルの数を取得する例で実装していきます。

また、一覧の表示については Composite パターン を用いています。

まずはディレクトリとファイルで継承する Component クラスです。

App/Component/FileSystem.php
<?php

namespace App\Component;

use App\Visitors\Interfaces\VisitorInterface;

abstract class FileSystem
{
    private $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public abstract function add(FileSystem $file);

    public function display()
    {
        echo sprintf("%s<br>\n", $this->getName());
    }

    public abstract function getChildren();

    public function accept(VisitorInterface $visitor)
    {
        $visitor->visit($this);
    }
}

ここでのポイントは2つ、getChildren() メソッドと accept() メソッドです。

getChildren() メソッドでは、ディレクトリとファイルのクラスそれぞれで定義させる為に抽象メソッドとして宣言しているだけですが、accept() メソッドでは、Visitor クラスの受け入れを行っています。

後で全体を俯瞰するとわかりやすいですが、ここで Visitor オブジェクトを受け取った後に、Visitor オブジェクトの visit() メソッドに自身を預けている、といった事を行っています。

次に、ディレクトリとファイルを扱うクラスです。

App/Composite/Dir.php
<?php

namespace App\Composite;

use App\Component\FileSystem;

class Dir extends FileSystem
{
    private $files;

    public function __construct($name)
    {
        parent::__construct($name);
        $this->files = [];
    }

    public function add(FileSystem $file)
    {
        array_push($this->files, $file);
    }

    public function display()
    {
        parent::display();
        foreach ($this->files as $file) {
            echo $file->display();
        }
    }

    public function getChildren()
    {
        return $this->files;
    }
}
App/Leaf/File.php
<?php

namespace App\Leaf;

use App\Component\FileSystem;

class File extends FileSystem
{
    public function __construct($name)
    {
        parent::__construct($name);
    }

    public function getName()
    {
        return parent::getName();
    }

    public function add(FileSystem $file)
    {
        throw new Exception('This method is not allowed.');
    }

    public function getChildren()
    {
        return array();
    }
}

ここでのポイントは、継承した FileSystem クラスの getChildren() メソッドを定義している点です。それぞれ、配列を返すようにしています。 Dir クラスの方では自身の持つ配列を返していますが、File クラスでは空の配列を投げています。

ちなみになぜ空配列なのかですが、File クラスはツリー構造の末端であるという事で、File クラス自身を返す事が出来ればよいからです。(= Dir クラスは複数の File オブジェクトを保有している反面、File クラスは自身しか存在しない)

そしてここからメインディッシュの Visitor クラスです。まずはインターフェイスを作成します。

App/Visitors/Interfaces/VisitorInterface.php
<?php

namespace App\Visitors\Interfaces;

use App\Component\FileSystem;

interface VisitorInterface
{
    public function visit(FileSystem $obj);
}

visit() メソッドのみです。 FileSystem クラスを受け取ります。

次に具象クラスです。 Visitor インターフェイスを実装します。ディレクトリやファイルの数を数えるクラスです。

App/Visitors/CountVisitor.php
<?php

namespace App\Visitors;

use App\Visitors\Interfaces\VisitorInterface;
use App\Component\FileSystem;

class CountVisitor implements VisitorInterface
{
    private $directories_cnt = 0;
    private $files_cnt = 0;

    public function visit(FileSystem $obj)
    {
        $class_name = basename(strtr(get_class($obj), '\\', '/'));
        if ($class_name === 'Dir') {
            $this->directories_cnt++;
        } else if ($class_name === 'File') {
            $this->files_cnt++;
        }

        foreach ($obj->getChildren() as $c) {
            $this->visit($c);
        }
    }

    public function getDirectoriesCnt()
    {
        return $this->directories_cnt;
    }

    public function getFilesCnt()
    {
        return $this->files_cnt;
    }
}

Visit() メソッドでは、受け取ったクラスが Dir クラスか File クラスかを判定し、数をカウントしています。そしてそれぞれの get メソッドで数を返却する。という流れです。

実装が完了したので、クライアントから利用してみます。

index.php
<?php

use App\Composite\Dir;
use App\Leaf\File;
use App\Visitors\CountVisitor;

$root_dir = new Dir('/');

$file_1 = new File('file_01.txt');
$root_dir->add($file_1);

$file_2 = new File('file_02.txt');
$root_dir->add($file_2);

$dir_1 = new Dir('dir01/');
$root_dir->add($dir_1);

$dir_2 = new Dir('dir02/');
$dir_2->add(new File('file_03.txt'));
$root_dir->add($dir_2);

$dir_3 = new Dir('dir03/');
$dir_3->add(new File('file_04.txt'));
$dir_3->add(new File('file_05.txt'));
$dir_3->add(new File('file_06.txt'));

$dir_4 = new Dir('dir04/');
$dir_4->add(new File('file_07.txt'));
$dir_3->add($dir_4);

$root_dir->add($dir_3);

// 一覧の出力
$root_dir->display();

$visitor = new CountVisitor();
$root_dir->accept($visitor);

// 数の出力
echo sprintf('ディレクトリ数:%d<br>', $visitor->getDirectoriesCnt());
echo sprintf('ファイル数::%d<br>', $visitor->getFilesCnt());

結果は以下になります。

データの表示と、カウントの表示ロジックを分離させた状態で表示できている事が確認できました。

まとめ

「Visitor = 訪問者」を意味する通り、ある機能に対して、そのクラスに依存しない形で機能を付与したり利用できるデザインパターンです。デザインパターン自体は java で生まれたものですが、PHP で利用するにあたり、双方で出来る事出来ない事もあり、PHP ならではの取り回しがあります。

開発に置いての拡充性を考えた時にやはり、その規模が大きくなればなるほどこのパターンの良さが効いてきます。是非試してみてください。

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級