インタプリタ(Interpreter)


GoFのデザインパターン(Design Pattern)のひとつ、インタプリタ(Interpreter)をRubyのサンプルコードで紹介します。
インタプリタパターンはひとつひとつの問題はシンプルだが、組み合わさって複雑になるような場合に効果を発揮します。

🐯 インタプリタとは?

専用の言語を作り、その言語で得られた手順にもとづいて処理を実行していくデザインパターンです。

インタプリタには次の構成要素があります。

  • 抽象表現(AbstractExpression): 共通のインタフェースを定義
  • 終端(TerminalExpression): 終端を表現するクラス
  • 終端以外(NonterminalExpression): 非終端を表現するクラス
  • 状況、文脈(Context): 構文の解析を手助けする

🍮 サンプルソース

サンプルとして、ファイル検索用のインタプリタを書いていきます。
まずは、すべてのファイル検索のベースとなる最も単純なクラスを作成します。

# 命令・抽象的な表現(AbstractExpression)
# Expression: 共通するコードを持つ
class Expression
def |(other)
Or.new(self, other)
end
def &(other)
And.new(self, other)
end
end

続いて、すべてのファイル名を返すAllクラスを作成します。
evaluateメソッドの概要は次のとおりです。

  • Rubyの標準ライブラリfindを使ってディレクトリ内のファイル名を収集
  • Find.findを使うことで、サブフォルダまで含めたすべてのファイルを返す
require "find"
# 終端となる表現(構造木の葉) (TerminalExpression)
# All: すべてのファイルを返す
class All < Expression
def evaluate(dir)
results= []
Find.find(dir) do |p|
next unless File.file?(p)
results << p
end
results
end
end

続いて、与えられたパターンとマッチするすべてのファイル名を返すFileNameクラスを作成します。

# 終端となる表現(構造木の葉) (TerminalExpression)
# FileName: 与えられたパターンとマッチするすべてのファイル名を返す
class FileName < Expression
def initialize(pattern)
@pattern = pattern
end
def evaluate(dir)
results= []
Find.find(dir) do |p|
next unless File.file?(p)
# File.basename => ファイルパスからファイル名だけを抽出
name = File.basename(p)
# File.fnmatch => ファイル名がパターンにマッチした場合のみtrueを返す
results << p if File.fnmatch(@pattern, name)
end
results
end
end

さらに、指定したファイルサイズより大きいファイルを返すBiggerクラスを作成します。

# 終端となる表現(構造木の葉) (TerminalExpression)
# Bigger: 指定したファイルサイズより大きいファイルを返す
class Bigger < Expression
def initialize(size)
@size = size
end
def evaluate(dir)
results = []
Find.find(dir) do |p|
next unless File.file?(p)
results << p if( File.size(p) > @size)
end
results
end
end

書込可能なファイルを返すWritableクラスは次のようになります。

# 終端となる表現(構造木の葉) (TerminalExpression)
# Writable: 書込可能なファイルを返す
class Writable < Expression
def evaluate(dir)
results = []
Find.find(dir) do |p|
next unless File.file?(p)
results << p if( File.writable?(p) )
end
results
end
end

ここで、「書込ができないファイル」を探せるように、Writableクラスを否定できるNotクラスを作ります。Notクラスは、Biggerクラスにも適用できるので、指定したファイルサイズより小さいファイルを探せます。

# 終端以外の表現(構造木の節) NonterminalExpression
class Not < Expression
def initialize(expression)
@expression = expression
end
def evaluate(dir)
All.new.evaluate(dir) - @expression.evaluate(dir)
end
end

最後に2つのファイル検索式を「Or」「And」で結合するクラスを作ります。

# 終端以外の表現(構造木の節) NonterminalExpression
# Or: 2ファイル検索式をORで結合する
class Or < Expression
def initialize(expression1, expression2)
@expression1 = expression1
@expression2 = expression2
end
def evaluate(dir)
result1 = @expression1.evaluate(dir)
result2 = @expression2.evaluate(dir)
(result1 + result2).sort.uniq
end
end
# 終端以外の表現(構造木の節) NonterminalExpression
# And: 2ファイル検索式をANDで結合する
class And < Expression
def initialize(expression1, expression2)
@expression1 = expression1
@expression2 = expression2
end
def evaluate(dir)
result1 = @expression1.evaluate(dir)
result2 = @expression2.evaluate(dir)
(result1 & result2)
end
end

上のコードを使ってファイルを検索した結果を下に載せました。

# ===========================================
complex_expression1 = And.new(FileName.new('*.mp3'), FileName.new('big*'))
puts complex_expression1.evaluate('13_test_data')
#=> 13_test_data/big.mp3
#=> 13_test_data/big2.mp3
complex_expression2 = Bigger.new(1024)
puts complex_expression2.evaluate('13_test_data')
#=> 13_test_data/big.mp3
#=> 13_test_data/big2.mp3
#=> 13_test_data/subdir/other.mp3
complex_expression3 = FileName.new('*.mp3') & FileName.new('big*')
puts complex_expression3.evaluate('13_test_data')
#=> 13_test_data/big.mp3
#=> 13_test_data/big2.mp3
complex_expression4 = All.new
puts complex_expression4.evaluate('13_test_data')
#=> 13_test_data/big.mp3
#=> 13_test_data/big2.mp3
#=> 13_test_data/small.mp3
#=> 13_test_data/small1.txt
#=> 13_test_data/small2.txt
#=> 13_test_data/subdir/other.mp3
#=> 13_test_data/subdir/small.jpg

このように単純なインタプリタでも十分にファイル検索を実現できていることがわかります。

🐡 サンプルソース

😸 参考リンク

📚 おすすめの書籍