引用記事
この記事を書くきっかけになったブログです。
記事内の解説やソースコードは、こちらのブログと著者の公開リポジトリを参考にしています。
Do You PHP はてな〜[doyouphp][phpdp]PHPによるデザインパターン入門 - Interpreter~言語の文法表現を通訳する
概要
- 「interpreter」は「通訳者」「解釈者」という意味。
- 文法を解析し、その結果を利用して処理を行う。
- 文法で定義されている規則をクラスとして表し、それに対する振る舞いを併せて定義する。
- この規則は構文木における節や葉に相当する。そして、そのクラスのインスタンスを繋げることで構文木そのものを表現しつつ、構文木を処理する。
構成要素
AbstractExpressionクラス
- 構文木の要素に共通なAPIを定義するクラス。
TerminalExpressionクラス
- AbstractExpressionクラスのサブクラス。
- 構文木の葉に相当する末端の規則を表す。
NonterminalExpressionクラス。
- AbstractExpressionクラスのサブクラス。
- 構文木の節に相当する。
- 内部に他の規則へのリンクを保持している。
Contextクラス
- 構文木を処理するために必要な情報を保持するクラス。
Clientクラス
- AbstractExpressionクラスを利用するクラス。
- 処理する構文木を作成したり、外部から与えられたりする。
実演
処理の流れ
- 構文の解析を各クラスで定義する。
- 構文は
「begin」「各コマンド」「end」
で構成される。(BNF(Backus Naur Form)と呼ばれる記法) - 上記の構文を
「<Job>」「<CommandList>」「<Command>」
に対応させ、それぞれをクラスとして定義する。 - beginで始まりendで終わる構文を解釈し、各コマンドを実行する。
- コマンドは
「diskspace, date, line」
の3種類を用意する。
ファイル構造
MyInterpreter
├── Command.php
├── CommandCommand.php
├── CommandListCommand.php
├── Context.php
├── JobCommand.php
└── my_client.php
ソースコード
AbstractExpressionクラス
Command.php
Command.php
<?php
namespace DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Context;
/**
* AbstractExpressionクラスに相当する
* インターフェイスのため、メソッドはサブクラスで実装する
*/
interface Command
{
public function execute(Context $context);
}
TerminalExpressionクラス
CommandCommand.php
CommandCommand.php
<?php
namespace DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Command;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Context;
/**
* TerminalExpressionクラスに相当する
* 末端の規則を定義する
* 各コマンドに対応する処理が定義されている
*/
class CommandCommand implements Command
{
public function execute(Context $context)
{
$current_command = $context->getCurrentCommand();
// 要素が'diskspace'の場合は、カレントディレクトリの全体サイズを返す
if ($current_command === 'diskspace') {
echo disk_total_space('./').'<br>'."\n";
// 要素が'date'の場合は、年月日を返す
} elseif ($current_command === 'date') {
echo date('Y/m/d').'<br>'."\n";
// 要素が'line'の場合は、罫線を返す
} elseif ($current_command === 'line') {
echo '--------------------'.'<br>'."\n";
// それ以外の場合は例外を返す
} else {
throw new \RuntimeException($current_command.'は無効なコマンドです');
}
}
}
NonterminalExpressionクラス
JobCommand.php
JobCommand.php
<?php
namespace DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Command;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Context;
/**
* NonterminalExpressionクラスに相当する
* CommandListCommandクラスへのリンクを保持する
* 規則どおりbeginで始まっている構文は、コマンド実行のクラスへリンクされる
*/
class JobCommand implements Command
{
public function execute(Context $context)
{
// 配列の最初の要素の値が'begin'でなければ、例外メッセージを投げる
if ($context->getCurrentCommand() !== 'begin') {
throw new \RuntimeException($context->getCurrentCommand().'は不正なコマンドです');
}
// 配列の最初の要素の値が'begin'であれば、コマンド実行のクラスへリンクされる
// 次のCommandListCommandクラスのオブジェクトを生成する
$command_list = new CommandListCommand();
// カウンタを進め、'begin'の次の要素にアクセスできるようにする
$command_list->execute($context->next());
}
}
CommandListCommand.php
CommandListCommand.php
<?php
namespace DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Command;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Context;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\CommandCommand;
/**
* NonterminalExpressionクラスに相当する
* CommandCommandクラスへのリンクを保持する
* コマンド処理のクラスへリンクされ、'end'の時点で処理を終了する
*/
class CommandListCommand implements Command
{
public function execute(Context $context)
{
while (true) {
// 配列の要素を順に返す
// 配列の要素数だけ繰り返される
$current_command = $context->getCurrentCommand();
// 要素が存在しない場合は例外メッセージを投げる
if (is_null($current_command)) {
throw new \RuntimeException('endが見つかりません');
// 要素の値が'end'の場合は処理を終了する
} elseif ($current_command === 'end') {
break;
// そうでなければ各コマンドを実行する
// 次のCommandCommandクラスのオブジェクトを生成する
} else {
$command = new CommandCommand();
$command->execute($context);
}
// 配列を回すカウンタを加算する
$context->next();
}
}
}
Contextクラス
Context.php
Context.php
<?php
namespace DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter;
/**
* Contextクラス
*/
class Context
{
private $commands;
private $current_index = 0;
private $max_index = 0;
public function __construct($command)
{
// $commandから前後の空白を除去し、半角スペース区切りの文字を配列の要素として格納する
$this->commands = preg_split('/[\s]/', trim($command));
// 配列の要素数を$max_indexにセットする
$this->max_index = count($this->commands);
}
public function next()
{
// 最初のcurrent_indexは0
// 次回以降は1ずつ加算される
$this->current_index++;
return $this;
}
public function getCurrentCommand()
{
// $current_indexの値が配列の添字として存在しなければ、何も返さない
if (!array_key_exists($this->current_index, $this->commands)) {
return;
}
// $current_indexの値が配列の添字として存在する場合は、配列の値を返す
return trim($this->commands[$this->current_index]);
}
}
Clientクラス
my_client.php
my_client.php
<?php
namespace DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter;
require dirname(dirname(__DIR__)).'/vendor/autoload.php';
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Context;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\Command;
use DoYouPhp\PhpDesignPattern\Interpreter\MyInterpreter\JobCommand;
function execute($command)
{
// 元の文字列を表示する
echo '■'.$command.'<br>'."\n";
// JobCommandクラスのオブジェクトを生成する
$job = new JobCommand();
// 構文が不正な場合は例外を返す
try {
$job->execute(new Context($command));
} catch (\Exception $e) {
echo $e->getMessage().'<br>'."\n";
}
}
execute('begin date end');
execute('begin date line diskspace end');
execute('begin diskspace date end');
// 以下3つの文字列は例外が発生する
execute('date end');
execute('begin date');
execute('begin date string end');