「and」式の新しいテストをリスト17に示します。現時点では、このテストは失敗します。
@Test public void andExpression() { Expression expression = parser.parse("olderThan 03/31/2008 and contains java"); And and = (And)expression; OlderThan olderThan = (OlderThan)and.getLeft(); assertDate(2008, Calendar.MARCH, 31, olderThan.getDate()); Contains contains = (Contains)and.getRight(); assertKeywords((Contains)contains, "java"); }
このテストに合格するパーサーを実装するには、アルゴリズムを少し見直す必要があります。ただ、最低限必要なコンポーネントは作ってあるので、それほど時間はかかりません(リスト18)。
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); expressionTypes.put("and", And.class); } private Expression current; private List<Expression> expressions = new ArrayList<Expression>(); public Expression parse(String expressionText) { String[] tokens = expressionText.split(" "); for (String token:tokens) if (isKeyword(token)) { storeArguments(); current = createExpression(token); if (isProcessingBinaryExpression()) { And and = (And)pop(); Expression left = pop(); and.set(left, current); push(and); } else push(current); } else arguments.add(token); storeArguments(); return pop(); } private boolean isProcessingBinaryExpression() { return expressions.size() == 2; } private void storeArguments() { if (current == null) return; current.setArgs(arguments); arguments = new ArrayList<String>(); } private boolean push(Expression expression) { return expressions.add(expression); } private Expression pop() { return expressions.remove(expressions.size() - 1); } 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); } }
もう一息です。当然、「and」に加えて「or」のサポートについてもテストし、さらにもっと複雑な式の場合でも正しく動作することを確認するための大きなテストをいくつか実行する必要があります。Parser
クラスの最終バージョンをリスト19に示します。また、リファクタリングしたExpression実装クラスの階層の一部も示します。
// Expression.java import java.util.*; public interface Expression { void setArgs(List<String> args); boolean evaluate(Document document); } // BinaryExpression import java.util.*; abstract public class BinaryExpression implements Expression { protected Expression leftExpression; protected Expression rightExpression; public void set(Expression leftExpression, Expression rightExpression) { this.leftExpression = leftExpression; this.rightExpression = rightExpression; } abstract public boolean evaluate(Document document); public Expression getLeft() { return leftExpression; } public Expression getRight() { return rightExpression; } @Override public void setArgs(List<String> args) { // violation of Liskov!OK for now. } } // Or.java public class Or extends BinaryExpression implements Expression { public boolean evaluate(Document document) { return leftExpression.evaluate(document) || rightExpression.evaluate(document); } } // KeywordExpression import java.util.*; abstract public class KeywordExpression implements Expression { protected List<String> keywords; @Override abstract public boolean evaluate(Document document); @Override public void setArgs(List<String> keywords) { this.keywords = keywords; } String[] getKeywords() { return (String[])keywords.toArray(new String[0]); } } public class Contains extends KeywordExpression implements Expression { @Override public boolean evaluate(Document document) { return document.contains(ListUtil.asArray(keywords)); } } // Parser.java 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("excludes", Excludes.class); expressionTypes.put("olderThan", OlderThan.class); expressionTypes.put("and", And.class); expressionTypes.put("or", Or.class); } private Expression current; private List<Expression> expressions = new ArrayList<Expression>(); public Expression parse(String expressionText) { String[] tokens = expressionText.split(" "); for (String token: tokens) if (isKeyword(token)) { storeArguments(); newExpression(token); } else arguments.add(token); storeArguments(); return pop(); } private void newExpression(String token) { current = createExpression(token); if (isProcessingBinaryExpression()) { BinaryExpression binary = (BinaryExpression)pop(); Expression left = pop(); binary.set(left, current); push(binary); } else push(current); } private boolean isProcessingBinaryExpression() { return expressions.size() == 2; } private void storeArguments() { if (current == null) return; current.setArgs(arguments); arguments = new ArrayList<String>(); } private boolean push(Expression expression) { return expressions.add(expression); } private Expression pop() { return expressions.remove(expressions.size() - 1); } 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); } }
全体的に見れば、なかなかの出来です。ソリューション全体の試作には満足できましたし、できあがったクラスでも、解析のときについてまわる複雑さを、ある程度切り離すことに成功しています。確かに、足りない点も多々あります(特にエラー処理)。しかし、残った点についても、かなり簡単な方法で試行してみることができると思います。