「Interpreter」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

  • LINEで送る
「Interpreter」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

 本記事ではGoFのデザインパターンのプログラムの振る舞いに関するパターンの一つである「Interpreter」パターンを解説します。このパターンを一言で説明するならば、「何らかの形式で書かれたファイルの中身を解析・表現し、言語の文法をオブジェクトで表現する方法」と言えるでしょう。「Interpreter」パターンをサンプルを踏まえて解説します。

Interpreter」パターンとは

 Interpreterパターンは、「通訳者」や「解釈者」という意味があります。ある言語や文法を解釈し、その意味を読み取って実行するデザインパターンです。特に規則的な文法に従ってデータを処理する場合に役立ちます。このパターンは、定義された文法やルールに従い、複数のインスタンスを組み合わせて結果を得るために使用されます。

 日常生活での例として、料理のレシピを考えてみましょう。料理のレシピは「材料」と「手順」という一種の「文法」に従って書かれています。例えば、「卵を割る」「牛乳を加える」「混ぜる」という一連の動作が記述されている場合、その手順に従って料理をするはずです。

 Interpreterパターンでは、レシピを構成する各手順を「個々の式(表現)」として捉え、それらを順に解釈することにより料理が完成する、といった流れです。もしレシピを「英語」や「フランス語」など異なる言語で書かれていたとしても、その文法が分かっていれば正しい料理を作れるように、このパターンでは言語やルールを解釈し、実行可能な形に変換する役割を持ちます。

Interpreter 手順をクラスとして表現、クラスを順に理解してパンケーキができる

Interpreter」パターンの書き方

 上述した料理のレシピの例を用いて実装してみましょう。今回はパンケーキをInterpreterパターンで表現します。パンケーキの料理手順は以下になります。

  1. 小麦粉を計量する
  2. 卵を割る
  3. 牛乳を加える
  4. 材料を混ぜる
  5. フライパンで焼く

 Interpreterパターンを実装していく上で、大きく分けて4つのクラスを利用していきます。

クラス名役割実装例でのクラス名
Context文脈情報を保持するクラスCookingContext
AbstractExpression抽象式(調理の手順を表すインターフェース)CookingExpression
TerminalExpression終端式(具体的な調理手順)
AbstractExpressionを実装
MeasureFlourExpression
CrackEggExpression
AddMilkExpression
MixIngredientsExpression
※すべて調理手順を表すクラス
NonTerminalExpression非終端式(調理手順を組み合わせる)
AbstractExpressionを実装
SequenceExpression

 まずはContextであるCookingContextクラスを見ていきます。

// Context: 文脈情報を保持するクラス
class CookingContext {
    private String currentStep;

    public String getCurrentStep() {
        return currentStep;
    }

    public void setCurrentStep(String currentStep) {
        this.currentStep = currentStep;
    }
}

 CookingContextクラスは調理の進行状況を保持するクラスです。現在の手順(currentStep)を保持し、それを他のクラスで参照したり更新したりします。

 次に調理の手順を表すインターフェースのAbstractExpressionに該当するCookingExpressionです。

// AbstractExpression: 抽象式(調理の手順を表す抽象クラス)
public abstract class CookingExpression {
    protected CookingContext context;
    abstract void  interpret(CookingContext context);
}

 このクラスは調理手順を解釈するためのインターフェースです。CookingContextのオブジェクトをフィールドに持っています。interpret(CookingContext context)メソッドを実装して具体的な手順を解釈します。

 次にAbstractExpressionのインターフェースを実装した実際の手順であるTerminalExpressionです。

// TerminalExpression: 終端式(具体的な調理手順)
class MeasureFlourExpression implements CookingExpression {
    @Override
    public void interpret(CookingContext context) {
        context.setCurrentStep("小麦粉を計量します");
        System.out.println(context.getCurrentStep());
    }
}

class CrackEggExpression implements CookingExpression {
    @Override
    public void interpret(CookingContext context) {
        context.setCurrentStep("卵を割ります");
        System.out.println(context.getCurrentStep());
    }
}

class AddMilkExpression implements CookingExpression {
    @Override
    public void interpret(CookingContext context) {
        context.setCurrentStep("牛乳を加えます");
        System.out.println(context.getCurrentStep());
    }
}

class MixIngredientsExpression implements CookingExpression {
    @Override
    public void interpret(CookingContext context) {
        context.setCurrentStep("材料を混ぜます");
        System.out.println(context.getCurrentStep());
    }
}

class CookPancakeExpression implements CookingExpression {
    @Override
    public void interpret(CookingContext context) {
        context.setCurrentStep("フライパンでパンケーキを焼きます");
        System.out.println(context.getCurrentStep());
    }
}

 料理の手順はすべてCookingExpressionを継承してinterpret(CookingContext context)メソッドを実装します。今回の例ではcontextのsetCurrentStep()メソッドに手順の文字列を格納します。その後、現在の手順を取得して標準出力を行います。

 最後にNonTerminalExpressionであるSequenceExpressionを実装します。

// NonTerminalExpression: 非終端式(調理手順を組み合わせる)
public class SequenceExpression extends CookingExpression {

    private CookingExpression[] steps;

    public SequenceExpression(CookingExpression... steps) {
        this.steps = steps;
    }

    @Override
    public void interpret(CookingContext context) {
        for (CookingExpression step : steps) {
            step.interpret(context);
        }
    }
}

 このクラスは調理手順とは違い、各手順をまとめて実行するクラスです。複数のCookingExpression(調理手順)を引数として受け取り、それを順番に解釈して実行します。コンストラクタでCookingExpressionを無限に格納できます。interpret()ではすべてのstepのinterpret()メソッドを実行します。

 それではこれらのクラスを用いて、調理を実施します。

public class Main {
    public static void main(String[] args) {
        CookingContext context = new CookingContext();

        // 調理の手順を構築
        CookingExpression measureFlour = new MeasureFlourExpression();
        CookingExpression crackEgg = new CrackEggExpression();
        CookingExpression addMilk = new AddMilkExpression();
        CookingExpression mix = new MixIngredientsExpression();
        CookingExpression cookPancake = new CookPancakeExpression();

        // 一連の手順をまとめて解釈
        CookingExpression recipe = new SequenceExpression(measureFlour, crackEgg, addMilk, mix, cookPancake);
        recipe.interpret(context);

    }
}
>> 実行結果
小麦粉を計量します
卵を割ります
牛乳を加えます
材料を混ぜます
フライパンでパンケーキを焼きます

 今回のコードでは、パンケーキを作るための具体的な手順をそれぞれのクラスで表現し、SequenceExpressionでそれらの手順を順番に解釈します。例えば、他の料理レシピに変えたい場合や新しい手順を追加したい場合、個々の手順クラスを再利用することができ、柔軟に対応できます。

まとめ

 Interpreterパターンの利点と欠点は以下になります。

利点欠点
・解釈すべき文法やルールが変更されても、既存のコードに大きな影響を与えることなく対抗できるため拡張性が高い。

・各Expressionクラスは独立しているため再利用性が高い。既存のクラスを組み合わせて新しい解釈が可能。

・複雑な式やルールを個別のクラスに分割することでコードの可読性が向上する
・式をオブジェクトとして扱うため、大量のルールを扱う場合、クラス数が増えパフォーマンスの低下を引き起こす可能性がある

・シンプルな構文やルールには適しているが、非常に複雑な文法や言語を表現する際には、パターンが不適切になる場合がある。

Interpreterパターンが有効なシチュエーション

  • 小さなドメイン特化言語(DSL: Domain-Specific Language)や、明確な構文やルールが決まっている場合
  • 再帰的に処理を進める必要がある場合。たとえば、文法解析や階層的な命令セットの処理に有効
  • 複雑すぎないが、ある程度の規則やルールに基づいて解釈が必要なシステム(例:四則演算や文法解析など)の場合。頻繁な変更はないが拡張や変更の可能性がある場合には特に適している。

 本記事で利用したサンプルケースのクラス図は下記になります。

interpret クラス図

最後に「Interpreter」パターンとは「何らかの形式で書かれたファイルの中身を解析・表現し、言語の文法をオブジェクトで表現する方法」です。うまく使っていきましょう。

 本記事で利用したコードは下記のgit上で公開しております。

https://github.com/tamotech1028/interpreter

最新の投稿

SNSでもご購読できます。

コメント

コメントを残す