Interpreter Pattern
2021. 12. 12. 10:53ㆍComputer 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 |