
GoFのデザインパターン(Design Pattern)の一つ、インタープリタ(Interpreter)をRubyのサンプルコードで紹介します。
インタープリタパターンは、1つ1つの問題はシンプルだが、組み合わさって複雑になるような場合に効果を発揮します。
インタープリタとは?
専用の言語を作り、その言語で得られた手順に基づいて処理を実行していくデザインパターンです。
インタープリタには次の構成要素があります。
抽象表現(AbstractExpression): 共通のインタフェースを定義
終端(TerminalExpression): 終端を表現するクラス
終端以外(NonterminalExpression): 非終端を表現するクラス
状況、文脈(Context): 構文の解析を手助けする
サンプルソース
サンプルとして、ファイル検索用のインタープリタを書いていきます。
まずは、すべてのファイル検索のベースとなる最も単純なクラスを作成します。
1
2
3
4
5
6
7
8
9
10
11
| # 命令・抽象的な表現(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を使うことで、サブフォルダまで含めたすべてのファイルを返す
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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クラスを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 終端となる表現(構造木の葉) (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クラスを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # 終端となる表現(構造木の葉) (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クラスは次のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
| # 終端となる表現(構造木の葉) (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クラスにも適用できるので、指定したファイルサイズより小さいファイルを探せるようになります。
1
2
3
4
5
6
7
8
9
10
| # 終端以外の表現(構造木の節) 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」で結合するクラスを作ります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # 終端以外の表現(構造木の節) 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
|
上のコードを使ってファイルを検索した結果を下に載せました。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # ===========================================
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
|
このように単純なインタープリタでも十分にファイル検索を実現できていることがわかります。
このサンプルソースはGitHubにも置いています。
サンプルソース(GitHub)
Special Thanks
デザインパターン-Interpreter
RubyでInterpreterパターン/HTML5のセクション構造に対応したWebページの生成
RubyのメタプログラミングでInterpreterパターンを実装しよう!
Amazon.co.jp: Rubyによるデザインパターン: Russ Olsen, ラス・オルセン, 小林 健一, 菅野 裕, 吉野 雅人, 山岸 夢人, 小島 努: 本
変更来歴
12/10 09:00 GitHubへのサンプルソースの設置。導入文の修正
12/10 09:55 サンプルソースの説明を追加
12/11 00:00 書籍へのリンクをAmazon アフィリエイトに変更
06/21 23:25 Ruby2.0.0対応、読みづらい部分を修正