Javaを鍛える|デザインパターン全23回 #16

Interpreterパターン|検索クエリの分岐地獄を解消する

  • Java
  • デザインパターン
  • Interpreter
  • Javaを鍛える

📋 この記事の目次
  1. このパターンは何のためにあるか
  2. Before
  3. After
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftの検索ボックスでは、ユーザーが「status:open assignee:me」のような簡易クエリ文字列を入力すると、その内容に応じてチケットを絞り込みます。ただこの実装は、スペース区切り・コロン区切りのルールに従って手動で文字列を解析し、条件ごとにswitch文で分岐させるものでした。ステータスや担当者といったフィルタ項目が増えるにつれて、parseQueryメソッド内の分岐が膨れ上がり、新規要件のたびに同じ箇所を修正せざるを得なくなっていました。

このパターンは何のためにあるか

Interpreterパターンは、構造化されたテキスト文字列を解釈し、意味のあるオブジェクトモデルへ変換するためのデザインパターンです。例えば「status:open assignee:me」という検索クエリ文字列が、「ステータスがオープンかつ担当者が自分自身」というチケットの絞り込み条件オブジェクトに変換されるような場面で使います。

Before

import java.util.ArrayList;
import java.util.List;

public class BeforeMain {
    public static void main(String[] args) {
        String query = "status:open assignee:me";
        TicketSearchCondition condition = parseQuery(query);
        System.out.println("Status: " + condition.statusFilter);
        System.out.println("Assignee: " + condition.assigneeFilter);
    }

    static class TicketSearchCondition {
        String statusFilter;
        String assigneeFilter;
    }

    static TicketSearchCondition parseQuery(String query) {
        TicketSearchCondition result = new TicketSearchCondition();
        if (query == null || query.isEmpty()) {
            return result;
        }
        String[] parts = query.split(" ");
        for (String part : parts) {
            int colonIndex = part.indexOf(":");
            if (colonIndex < 0) {
                continue;
            }
            String key = part.substring(0, colonIndex);
            String value = part.substring(colonIndex + 1);
            switch (key) {
                case "status":
                    result.statusFilter = value;
                    break;
                case "assignee":
                    result.assigneeFilter = value;
                    break;
            }
        }
        return result;
    }
}

parseQueryメソッドがクエリ文字列の解析ロジックと条件オブジェクトへのマッピングを一身に引き受けているため、新しいフィルタ項目(例:priority:high)を追加するたびにswitch文の分岐が増え、メソッド本体が肥大化していきます。ルール変更時もここを修正する必要があり、単一責任の原則にも抵触している状態です。

After

import java.util.ArrayList;
import java.util.List;

public class AfterMain {
    public static void main(String[] args) {
        String query = "status:open assignee:me";
        TicketSearchCondition condition = parseQuery(query);
        System.out.println("Status: " + condition.statusFilter);
        System.out.println("Assignee: " + condition.assigneeFilter);
    }

    interface QueryExpression {
        void interpret(String input, TicketSearchCondition context);
    }

    static class TicketSearchCondition {
        String statusFilter;
        String assigneeFilter;
    }

    static class StatusQueryExpression implements QueryExpression {
        @Override
        public void interpret(String input, TicketSearchCondition context) {
            if (input.startsWith("status:")) {
                context.statusFilter = input.substring("status:".length());
            }
        }
    }

    static class AssigneeQueryExpression implements QueryExpression {
        @Override
        public void interpret(String input, TicketSearchCondition context) {
            if (input.startsWith("assignee:")) {
                context.assigneeFilter = input.substring("assignee:".length());
            }
        }
    }

    static TicketSearchCondition parseQuery(String query) {
        TicketSearchCondition result = new TicketSearchCondition();
        if (query == null || query.isEmpty()) {
            return result;
        }
        String[] parts = query.split(" ");
        List<QueryExpression> expressions = createExpressions();
        for (String part : parts) {
            for (QueryExpression expr : expressions) {
                expr.interpret(part, result);
            }
        }
        return result;
    }

    static List<QueryExpression> createExpressions() {
        List<QueryExpression> list = new ArrayList<>();
        list.add(new StatusQueryExpression());
        list.add(new AssigneeQueryExpression());
        return list;
    }
}

実行結果は以下の通りです。

Status: open
Assignee: me

parseQueryはクエリ文字列を分割し、各パートを順にInterpreterへ渡すだけになりました。ステータスや担当者といった項目ごとに専用クラスが責務を持ち、新規フィルタの追加時はQueryExpressionの実装クラスを1つ追加し、createExpressionsに登録するだけで済むようになりました。

🧓 ベテランの現場コラム Interpreterパターンは、汎用のプログラミング言語を作ろうとするのではなく、「限られた文法を持つ小さなDSL(ドメイン特化言語)」を解釈する場面で使うと効果的です。検索クエリのような、あらかじめ文法が決まっている小規模な構文にはよく合いますが、条件のネストや論理演算子(AND/OR)まで扱おうとすると、一気に複雑になる点には注意してください。

こんな場面で使う/使わない

  • 使う: 検索クエリや設定ファイルなど、一定のルールに従って解釈できる文字列をオブジェクトモデルへ変換したい場合
  • 使わない: パースロジックが複雑でネストした条件分岐や再帰的構造を持つ場合(正規表現や専用のパーサーライブラリの方が適切なことが多い)

まとめ

  • Interpreterパターンでは、テキストの各要素をExpressionとして表現し、interpretメソッドで解釈処理を行う
  • 構造化された文字列を解釈する際は、分割してから各Interpreterに順に渡す、という基本フローになる
  • 個別のInterpreterクラスは単一責任を持ち、新規文法追加時には既存コードを変更せずに実装を追加できる

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。