酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

インタープリタ Ruby 2.0.0 デザインパターン速攻習得[Interpreter][Design Pattern]

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対応、読みづらい部分を修正

おすすめの書籍