RitoLabo

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

  • 公開:
  • 更新:
  • カテゴリ: PHP DesignPatterns
  • タグ: PHP,Behavior,DesignPatterns,Visitor

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

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パターンサンプル結果出力

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

まとめ

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

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