CodeZine(コードジン)

特集ページ一覧

デザインパターンの使い方: Interpreter

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2008/12/02 14:00

目次

解析

 残念ながら、Interpreterパターンの解説において、このような複合構造をそもそもどうやって作成するかという点が語られることはほとんどありません。今回は私の趣味で、文字列を受け取って適切なインタープリタ階層を生成する、簡単なパーサーを試作してみることにします。手始めに、1個の終端式を解析できることを確認するための簡単なテストを作成します(リスト8)。

リスト8 最初に作成したパーサー検証テスト
import static org.junit.Assert.*;
import org.junit.*;

public class ParserTest {
   @Test
   public void singleExpression() {
      Parser parser = new Parser();
      Expression expression = parser.parse("contains text");
      assertEquals(Contains.class, expression.getClass());
      Contains contains = (Contains)expression;
      String[] keywords = contains.getKeywords();
      assertEquals("text", keywords[0]);
   }
}

 このテストに合格するのは簡単です(リスト9)。

リスト9 ごく基本的なパーサー実装
public class Parser {
   public Expression parse(String expression) {
      String[] tokens = expression.split(" ");
      return new Contains(tokens[1]);
   }
}

 テストをもう少しだけ複雑にしてみましょう(リスト10)。「contains」キーワードに複数のキーワードを指定できることを確認します。

リスト10 複数のキーワードのサポートについてのテスト
import static org.junit.Assert.*;
import org.junit.*;

public class ParserTest {
   private Parser parser;

   @Before
   public void createParser() {
      parser = new Parser();
   }

   @Test
   public void singleContainsExpression() {
      Expression expression = parser.parse("contains text");
      assertKeywords((Contains)expression, "text");
   }

   @Test
   public void containsMultipleKeywords() {
      Expression expression = parser.parse("contains text bozo");
      assertKeywords((Contains)expression, "text", "bozo");
   }

   private void assertKeywords(Contains contains,
                               String... expected) {
      assertArrayEquals(expected, contains.getKeywords());
   }
}

 リスト11に、複数のキーワードをサポートするための実装を示します。注意してほしいのは、Containsクラスのコンストラクタを、Stringの配列を受け取るように変更する必要がある点です。

リスト11 複数のキーワードをサポートするパーサー
import java.util.*;

public class Parser {
   private List<String> arguments = new ArrayList<String>();

   public Expression parse(String expression) {
      String[] tokens = expression.split(" ");
      for (String token: tokens) {
         if (!token.equals("contains"))
            pushArgument(token);
      }
      return new Contains
         ((String[])arguments.toArray(new String[0]));
   }

   private void pushArgument(String token) {
      arguments.add(token);
   }
}

 次に実装が簡単なのは、もう1つの終端式である「olderThan」でしょう。リスト12と13に、olderThanを追加したテストと、修正後のParserクラスを示します。

リスト12 もう1つの終端式のサポートを確認するテスト
import static org.junit.Assert.*;
import java.util.*;
import org.junit.*;

public class ParserTest {
   private Parser parser;

   @Before
   public void createParser() {
      parser = new Parser();
   }

   @Test
   public void singleContainsExpression() {
      Expression expression = parser.parse("contains text");
      assertKeywords((Contains)expression, "text");
   }

   @Test
   public void containsMultipleKeywords() {
      Expression expression = parser.parse("contains text bozo");
      assertKeywords((Contains)expression, "text", "bozo");
   }

   @Test
   public void olderThan() {
      Expression expression = parser.parse("olderThan 03/31/2008");
      OlderThan olderThan = (OlderThan)expression;
      assertDate(2008, Calendar.MARCH, 31, olderThan.getDate());
   }

   private void assertDate(int year, int month,
                           int dayOfMonth, Date date) {
      Calendar calendar = GregorianCalendar.getInstance();
      calendar.setTime(date);
      assertEquals(year, calendar.get(Calendar.YEAR));
      assertEquals(month, calendar.get(Calendar.MONTH));
      assertEquals(dayOfMonth,
                   calendar.get(Calendar.DAY_OF_MONTH));
   }

   private void assertKeywords(Contains contains,
                               String... expected) {
      assertArrayEquals(expected, contains.getKeywords());
   }
}
リスト13 もう1つの終端式をサポートするパーサー
import java.text.*;
import java.util.*;

public class Parser {
   private List<String> arguments = new ArrayList<String>();

   public Expression parse(String expression) {
      String command = null;
      String[] tokens = expression.split(" ");
      for (String token: tokens)
         if (isKeyword(token))
            command = token;
         else
            pushArgument(token);
      return createExpression(command);
   }

   private Expression createExpression(String command) {
      if (command.equals("contains"))
         return new Contains
            ((String[])arguments.toArray(new String[0]));
      if (command.equals("olderThan"))
         return new OlderThan(parseDate(arguments.get(0)));
      return null;
   }

   private Date parseDate(String textDate) {
      try {
         return new
            SimpleDateFormat("MM/dd/yyyy").parse(textDate);
      } catch (ParseException unexpected) {
         return null;
      }
   }

   private boolean isKeyword(String token) {
      return token.equals("contains") ||
         token.equals("olderThan");
   }

   private void pushArgument(String token) {
      arguments.add(token);
   }
}

 なんだか、ごちゃごちゃしたパーサーになってきてしまいました。既にお気づきの人もいるでしょうが、このParserクラスは責任の切り分けがうまくできていません。というのも、関連する式オブジェクトごとに異なる引数の解析方法を、このクラス自身が把握していなければならないからです。また、コマンドを表す文字列を保持しておき、後で引数をアタッチできる段階になったときにコマンドをインスタンス化するという今のやり方も好ましくありません。おそらく、JavaBeansの規約に従う方が適切でしょう。そこで、個々のExpression実装クラスを修正し、引数のないコンストラクタと引数のセッターをサポートするようにしたいと思います。

 しばらく実装とテストを繰り返してリファクタリングを行った後、前よりすっきりした例ができあがりました。リスト14をご覧ください。

リスト14 リファクタリング後のパーサー
import java.util.*;

public class Parser {
   private List<String> arguments = new ArrayList<String>();

   public Expression parse(String expressionText) {
      String command = null;
      String[] tokens = expressionText.split(" ");
      for (String token: tokens)
         if (isKeyword(token))
            command = token;
         else
            arguments.add(token);

      Expression expression = createExpression(command);
      expression.setArgs(arguments);
      return expression;

   }

   private Expression createExpression(String command) {
      if (command.equals("contains"))
         return new Contains();
      if (command.equals("olderThan"))
         return new OlderThan();
      return null;
   }

   private boolean isKeyword(String token) {
      return token.equals("contains") ||
         token.equals("olderThan");
   }
}

 それぞれのExpression実装クラスに少し手を加え、引数のないコンストラクタのサポートと、setArgsメソッドを追加します(リスト15)。

リスト15 修正後のExpression型クラスの例
import java.util.*;

public class Contains implements Expression {
   private List<String> keywords = new ArrayList<String>();

   @Override
   public boolean evaluate(Document document) {
      return document.contains
         ((String[])keywords.toArray(new String[0]));
   }

   String[] getKeywords() {
      return (String[])keywords.toArray(new String[0]);
   }

   @Override
   public void setArgs(List<String> args) {
      this.keywords = args;
   }
}

 これでよくなりました。ただ、まだ少し冗長な部分が残っているので、これをどうにかしなくてはなりません。Expression型のクラスを新しく追加したときに、createExpressionisKeywordの両方にコードを追加する必要があるからです。これは面倒なばかりでなく、リスクも増やします。そこで、少しだけリフレクションを使用し、両方のメソッドのコードを分離することにします。この方法ではマップを1つ利用しますが、最初にマップの初期化が必要です(リスト16)。これにより、新しい式クラスを追加するときも、コードの1か所を変更するだけで済みます。

リスト16 適所でリフレクションを使用
import java.util.*;

public class Parser {
   private List<String> arguments = new ArrayList<String>();
   private Map<String,Class<? extends Expression>>
      expressionTypes = new HashMap<String,Class<?
      extends Expression>>();
   {
      expressionTypes.put("contains", Contains.class);
      expressionTypes.put("olderThan", OlderThan.class);
   }

   public Expression parse(String expressionText) {
      String command = null;
      String[] tokens = expressionText.split(" ");
      for (String token: tokens)
         if (isKeyword(token))
            command = token;
         else
            arguments.add(token);

      Expression expression = createExpression(command);
      expression.setArgs(arguments);
      return expression;

   }

   private Expression createExpression(String command) {
      try {
         return expressionTypes.get(command).newInstance();
      } catch (Exception e) {
         throw new RuntimeException(e);
      }
   }

   private boolean isKeyword(String token) {
      return expressionTypes.containsKey(token);
   }
}

 悪くないです。生成部分のコードを別のファクトリクラスに分割するというやり方もありますが、それは別の機会に譲ることにします。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

バックナンバー

連載:デザインパターンの使い方

もっと読む

著者プロフィール

  • japan.internet.com(ジャパンインターネットコム)

    japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.com や EarthWeb.c...

  • Jeff Langr(Jeff Langr)

    本格的なソフトウェアの開発に四半世紀以上携わってきたベテランのソフトウェア開発者。『Agile Java: Crafting Code With Test-Driven Development』(Prentice Hall、2005年)と、他の1冊の著書がある。『Clean Code』(Uncle...

あなたにオススメ

All contents copyright © 2005-2022 Shoeisha Co., Ltd. All rights reserved. ver.1.5