Interpreter Pattern

2021. 12. 12. 10:53Computer Sciences/Design Patterns

개요

인터프리터는 프로그래밍 언어를 처음 배울 때 아마 많이 접했을 것이다. C, Java(명백히 말하면 아니지만)와 같은 언어를 컴파일 언어라고 하고 파이썬, 자바스크립트 같은 언어를 인터프리터 언어라고 한다. 인터프리터 언어는 컴파일이 아니라 코드 한 줄 한 줄을 해석하고 실행하기 때문에 간편하고 빠른 결과를 확인할 수 있다. 인터프리터 패턴은 이와 같이 간단한 문법을 해석할 때 사용되는 패턴이다. 말로 설명하는 것보다 코드로 이해하는 것이 더 빠르므로 바로 예제로 들어가자.

예제 - 자동차 전진 프로그램

자동차를 전진시키는 프로그램을 만들고자 한다. 커맨드로 움직임을 구현할 것인데 명령어 구성은 다음과 같다.

  • program: 프로그램을 시작하는 커맨드
  • repeat: 루프를 시작하는 커맨드
  • go: 현재 바라보는 방향으로 전진하는 커맨드
  • right: 오른쪽으로 방향을 전환하는 커맨드
  • left: 왼쪽으로 방향을 전환하는 커맨드
  • end: repeat의 범위 또는 program의 범위를 한정하는 커맨드

한 칸 전진

  • program go end

한 칸 전진 후 다시 시작 지점으로 복귀

  • program go right right 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

프로그램 문법 - 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

클래스 다이어그램

  • 클라이언트는 Context에 문자열을 입력하고 ProgramNode.parse()를 해야 하는 책임이 있다.

코드

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 ParseException {
        if (!token.equals(currentToken)) {
            throw new ParseException("Warning: " + token + " is expected, but " + currentToken + " is found.");
        }
        nextToken();
    }

    public int currentNumber() {
        int number = 0;
        try {
            number = Integer.parseInt(currentToken);
        } catch (NumberFormatException e) {
            throw new ParseException("Warning: " + e);
        }
        return number;
    }
}
public interface Node {
    void parse(Context context) throws ParseException;
}
public class ProgramNode implements Node {

    private Node commandListNode;

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

    @Override
    public String toString() {
        return "[program " + commandListNode + "]";
    }
}
import java.util.ArrayList;

public class CommandListNode implements Node {

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

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

    @Override
    public String toString() {
        return list.toString();
    }
}
public class CommandNode implements Node {

    private Node node;

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

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

}
public class RepeatCommandNode implements Node {

    private int number;
    private Node commandListNode;

    @Override
    public void parse(Context context) throws ParseException {
        context.skipToken("repeat");
        number = context.currentNumber();
        context.nextToken();
        commandListNode = new CommandListNode();
        commandListNode.parse(context);
    }

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

}
public class PrimitiveCommandNode implements Node {

    private String name;

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

    public String toString() {
        return name;
    }
}
public class ParseException extends RuntimeException {

    public ParseException(String msg) {
        super(msg);
    }

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

public class Main {

    public static void main(String[] args) throws Exception {
        try (BufferedReader br = new BufferedReader(new FileReader("program.txt"))) {
            String text;
            while ((text = br.readLine()) != null) {
                System.out.println("text = \\"" + text + "\\"");
                Node node = new ProgramNode();
                node.parse(new Context(text));
                System.out.println("node = " + node);
            }
        }
    }
}

장단점

장점

  • 각 문법 규칙을 클래스로 표현하기 때문에 언어를 쉽게 구현할 수 있다.
  • 문법이 클래스에 의해 구현되기 때문에 언어를 쉽게 변경하거나 확장할 수 있다.
  • 클래스 구조에 메서드만 추가하면 프로그램을 해석하는 기본 기능 외에 예쁘게 출력하는 기능이라던가, 더 나은 프로그램 확인 기능 같은 새로운 기능을 추가할 수 있다.

단점

  • 문법 규칙의 개수가 많아지면 매우 복잡해진다. 이때는 파서, 컴파일러를 사용하는 것이 낫다.

 

'Computer Sciences > Design Patterns' 카테고리의 다른 글

Memento Pattern  (0) 2021.12.11
Flyweight Pattern  (0) 2021.12.11
Visitor Pattern  (0) 2021.12.11
Prototype Pattern  (0) 2021.12.11
14. Chain of Responsibility Pattern  (0) 2021.11.14