Design Pattern | Interpreter Pattern (인터프리터 패턴)

Interpreter 패턴이란?

  • Interpreter라는 영어 단어는 통역이라는 의미이다.
  • Interpreter 패턴은, 어떠한 형식으로 쓰여진 파일의 내용을 “통역"의 역할을 하는 프로그램으로 해석 및 표현하는 방식이다.
  • GoF의 디자인 패턴에서는 행위에 대한 디자인 패턴으로 분류된다.

Interpreter 패턴 예제 프로그램

텍스트 파일에 쓰여진 언어를 구문 분석하는 프로그램이다.

Class Diagram
Interpreter Pattern Class Diagram

구문 분석 대상 텍스트로 사용하는 언어의 문법에서는 BNF 표기법을 사용한다.

<program> ::= program <command list>
<command list> ::= <command>* end
<command> ::= <repeat command> | <primitive command>
<repeat command> ::= repeat <number> <command list>
<primitive command> ::= go | right | left
  • <program> : 토큰 program 뒤에 커멘드의 열이 계속된다는 것을 의미한다.
  • <command list> : 이 0개 이상 반복한 뒤 토큰 end 가 계속된다는 것을 의미한다.
  • <command> : 반복 명령 또는 기본 명령 중 하나가 된다. ( | 는 or 를 나타낸다.)
  • <repeat command> : 토큰 repeat 의 후에 반복 회수가 계속되고, 커멘드의 열이 계속된다는 것을 의미한다.
  • <primitive command> : go 또는 right 또는 left 을 의미한다.

1. Context 클래스

구문 분석을 위한 전후 관계를 나타내는 클래스이다.

Context.java

package com.devkuma.designpattern.behavioral.interpreter;

import java.util.StringTokenizer;

public class Context {

    private StringTokenizer tokenizer;
    private String currentToken;

    public Context(String text) {
        tokenizer = new StringTokenizer(text);
        nextToken();
    }

    public String nextToken() {
        if (tokenizer.hasMoreTokens()) {
            currentToken = tokenizer.nextToken();
        } else {
            currentToken = null;
        }
        return currentToken;
    }

    public String currentToken() {
        return currentToken;
    }

    public void skipToken(String token) throws Exception {
        if (!token.equals(currentToken)) {
            throw new Exception("Warning: " + token + " is expected, but " + currentToken + " is found.");
        }
        nextToken();
    }

    public int currentNumber() throws Exception {
        int number = 0;
        try {
            number = Integer.parseInt(currentToken);
        } catch (NumberFormatException e) {
            throw new Exception("Warning: " + e);
        }
        return number;
    }
}

2. Node 클래스

구문 트리의 “노드"가 되는 클래스이다.

Node.java

package com.devkuma.designpattern.behavioral.interpreter;

public abstract class Node {
    public abstract void parse(Context context) throws Exception;
}

3. ProgramNode 클래스

<program> ::= program <command list>에 대응하는 클래스입니다. Node의 구현이다.

ProgramNode.java

package com.devkuma.designpattern.behavioral.interpreter;

// <program> ::= program <command list>
public class ProgramNode extends Node {

    private Node commandListNode;

    @Override
    public void parse(Context context) throws Exception {
        context.skipToken("program");
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }

    public String toString() {
        return "[program " + commandListNode + "]";
    }
}

4. CommandNode 클래스

<command> ::= <repeat command> | <primitive command>에 대응하는 클래스이다. Node의 구현이다.

CommandNode.java

// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {

    private Node node;
    
    @Override
    public void parse(Context context) throws Exception {
        if (context.currentToken().equals("repeat")) {
            node = new RepeatCommandNode();
            node.parse(context);
        } else {
            node = new PrimitiveCommandNode();
            node.parse(context);
        }
    }

    public String toString() {
        return node.toString();
    }
}

5. RepeatCommandNode 클래스

<repeat command> ::= repeat <number> <command list>에 대응하는 클래스이다. Node의 구현이다.

RepeatCommandNode.java

package com.devkuma.designpattern.behavioral.interpreter;

// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {

    private Node node;

    @Override
    public void parse(Context context) throws Exception {
        if (context.currentToken().equals("repeat")) {
            node = new RepeatCommandNode();
            node.parse(context);
        } else {
            node = new PrimitiveCommandNode();
            node.parse(context);
        }
    }

    public String toString() {
        return node.toString();
    }
}

6. CommandListNode 클래스

<command list> ::= <command>* end에 대응하는 클래스이다. Node의 구현이다.

CommandListNode.java

package com.devkuma.designpattern.behavioral.interpreter;

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

// <command list> ::= <command>* end
public class CommandListNode extends Node {

    private List<Node> list = new ArrayList<Node>();

    @Override
    public void parse(Context context) throws Exception {
        while (true) {
            if (context.currentToken() == null) {
                throw new Exception("Missing 'end'");
            } else if (context.currentToken().equals("end")) {
                context.skipToken("end");
                break;
            } else {
                Node commandNode = new CommandNode();
                commandNode.parse(context);
                list.add(commandNode);
            }
        }
    }

    public String toString() {
        return list.toString();
    }
}

7. PrimitiveCommandNode 클래스

<primitive command> ::= go | right | left에 대응하는 클래스이다. Node의 구현이다.

PrimitiveCommandNode.java

package com.devkuma.designpattern.behavioral.interpreter;

// <primitive command> ::= go | right | left
public class PrimitiveCommandNode extends Node {

    private String name;

    @Override
    public void parse(Context context) throws Exception {
        name = context.currentToken();
        context.skipToken(name);
        if (!name.equals("go") && !name.equals("right") && !name.equals("left")) {
            throw new Exception(name + " is undefined");
        }
    }

    public String toString() {
        return name;
    }
}

8. 구문 분석 대상 텍스트

구문 분석 대상이 되는 텍스트이다.

program.txt

program end
program go end
program go right go right go right go right end
program repeat 4 go right end end
program repeat 4 repeat 3 go right go left end right end end

9. Main 클래스

메인 처리를 실행하는 클래스이다.

Main.java

package com.devkuma.designpattern.behavioral.interpreter;

import java.io.BufferedReader;
import java.io.FileReader;

public class Main {
    public static void main(String[] args) {
        ClassLoader loader = Main.class.getClassLoader();
        String file = loader.getResource("interpreter/program.txt").getFile();

        try {
            BufferedReader reader = new BufferedReader(new FileReader(file));
            String text;
            while ((text = reader.readLine()) != null) {
                System.out.println("text = \"" + text + "\"");
                Node node = new ProgramNode();
                node.parse(new Context(text));
                System.out.println("node = " + node);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

10. 실행 결과

text = "program end"
node = [program []]
text = "program go end"
node = [program [go]]
text = "program go right go right go right go right end"
node = [program [go, right, go, right, go, right, go, right]]
text = "program repeat 4 go right end end"
node = [program [[repeat 4 [go, right]]]]
text = "program repeat 4 repeat 3 go right go left end right end end"
node = [program [[repeat 4 [[repeat 3 [go, right, go, left]], right]]]]

장점

Interpreter 패턴을 활용하면 규칙을 추가하고 변경할 수 있다.
인터프리터 패턴의 특징 중 하나는 하나의 규칙을 하나의 클래스로 표현하는 것이다. 즉, 새로운 규칙을 추가하는 경우는 Node 클래스의 서브 클래스를 추가하는 것만으로 좋아진다.
또, 규칙을 수정하는 경우도 Node 클래스의 서브 클래스만 수정하면 된다.