(행동 패턴) 해석기 패턴

인터프리터 패턴

인터프리터 패턴이란 무엇입니까?

프로그램을 작성할 때 때때로 프로그램의 전체 동작을 정의할 수 없습니다. 예를 들어, 브라우저를 구축할 때 웹사이트 디자이너가 원하는 웹 페이지 동작 방식에 대한 모든 것을 예측하는 것은 불가능합니다. 이 경우 JavaScript와 같은 해석된 언어를 통해 브라우저는 브라우저 프로그래머가 구현하지 않은 동작을 추가할 수 있습니다.

인터프리터 패턴을 사용하면 이 목적으로 사용되는 인터프리터를 작성할 수 있습니다. 먼저 언어의 문법을 설명하는 규칙에 대한 공식 문법을 정의합니다. 그리고 각 규칙은 클래스에 의해 구현됩니다. 이러한 클래스는 컨텍스트 개체를 공유하고, 컨텍스트 개체를 통해 입력을 받고, 변수 값을 저장하는 등의 작업을 수행합니다.

언제 사용

  • 객체 지향 컴파일러 구현에 자주 사용됩니다.
  • 인터프리터 패턴은 복합 패턴을 사용할 때 사용할 수 있습니다. 그러나 복합 패턴으로 정의된 클래스가 언어 구조를 정의하는 경우에만 인터프리터 패턴이라고 할 수 있습니다.
  • 때로는 런타임에 시스템 설정을 변경하는 데 사용됩니다.
  • 언어가 주어지면 인터프리터를 사용하여 문법적 표현을 정의하고 해당 표현을 사용하여 언어의 문장을 해석합니다.

구조


  • 해석기 정보(컨텍스트): 모든 표현에 사용되는 일반적인 정보를 포함합니다.
  • 추상 구문 트리 인터페이스(AbstractExpression): 우리가 표현하고 있는 문법을 나타냅니다. 컨텍스트가 포함되어 있습니다.
  • 종료기호(종료식) : 끝나는 식
  • Nonterminal Expression: 다른 표현식을 재귀적으로 참조하는 표현식.
  • 문장(클라이언트)을 나타내는 추상 구문 트리

장점과 단점

장점

  • 각각의 문법 규칙이 클래스로 표현되기 때문에 언어 구현이 용이합니다.
  • 문법은 클래스로 표현되기 때문에 언어를 쉽게 변경하거나 확장할 수 있습니다.
  • 프로그램을 해석하는 기본 기능 외에도 클래스 구조에 메서드를 추가하는 것만으로 명확한 출력이나 더 나은 프로그램 식별과 같은 새로운 기능을 추가할 수 있습니다.

불리

  • 문법 규칙이 많을수록 더 복잡해집니다.
  • 여러 문법이 생성되면 성능 저하가 발생합니다.

접미사 작업을 수행하는 코드를 사용하는 인터프리터 패턴을 살펴보겠습니다. (접미사 연산: “123+-” -> “1 – (2 + 3)”)

public class PostfixNotation {
    private final String expression;
    public PostfixNotation(String expression) { this.expression = expression; }

    public static void main(String() args) {
        PostfixNotation postfixNotation = new PostfixNotation("123+-");
        postfixNotation.calculate();
    }

    private void calculate() {
        Stack<Integer> numbers = new Stack<>();

        for (char c: this.expression.toCharArray()) {
            switch (c) {
                case '+':
                    numbers.push(numbers.pop() + numbers.pop());
                    break;
                case '-':
                    int right = numbers.pop();
                    int left = numbers.pop();
                    numbers.push(left - right);
                    break;
                default:
                    numbers.push(Integer.parseInt(c + ""));
            }
        }

        System.out.println(numbers.pop());
    }
}

“123+-“, 즉 “xyz+-“와 같은 문법 정의 연산을 재사용한다고 가정합니다.

엑스, 와이, 지변경되지 않은 자체 값을 반환합니다. 다음 단계 없이 자체 값 반환 터미널 표현오전.

한편, 운영자 ‘+‘, ‘‘는 다른 두 표현식을 해석한 다음(숫자를 숫자로, 텍스트를 문자로 바꾸는 것을 생각할 수 있음) 결과를 계산합니다. 비말단 표현오전.

이 예제를 사용하여 문자열 접미사 작업을 수행하는 인터프리터 패턴을 구현합니다.

먼저 PostFixExpression 인터페이스를 만듭니다.

public interface PostFixExpression {
    int interpret(Map<Character, Integer> context);
}

모든 식은 위의 인터페이스를 구현해야 합니다.

먼저 값을 있는 그대로 반환하는 터미널 표현식인 VariableExpression을 만듭니다.

public class VariableExpression implements PostFixExpression {
    
    private Character character;
    
    public VariableExpression(Character character) {
        this.character = character;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return context.get(this.character); // 생성자 인자로 받은 character 의 키를 가져온다.
    }
}

다음으로 비단말식인 ​​더하기 및 빼기 식을 만듭니다.

public class PlusExpression implements PostFixExpression{

    private PostFixExpression left;
    private PostFixExpression right;

    public PlusExpression(PostFixExpression left, PlusExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return left.interpret(context) + right.interpret(context);
    }
}

public class MinusExpression implements PostFixExpression{

    private PostFixExpression left;
    private PostFixExpression right;

    public MinusExpression(PostFixExpression left, PostFixExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Map<Character, Integer> context) {
        return left.interpret(context) - right.interpret(context);
    }
}

이제 표현식을 사용하여 해석하는 파서 클래스를 만들어 보겠습니다. 예제에서 볼 수 있듯이 스택을 통해 구현됩니다.

public class PostFixParser {

    public static PostFixExpression parse(String expression) {
        Stack<PostFixExpression> stack = new Stack<>();
        for (char c: expression.toCharArray()) {
            stack.push(getExpression(c, stack));
        }
        return stack.pop();
    }

    private static PostFixExpression getExpression(char c, Stack<PostFixExpression> stack) {
        switch (c) {
            case '+' :
                return new PlusExpression(stack.pop(), stack.pop());
            case '-' :
                // 스택에서 먼저 빠져 나오는 값부터 계산해야 후위 연산의 순서가 맞게 된다.
                PostFixExpression right = stack.pop();
                PostFixExpression left = stack.pop();
                return new MinusExpression(left, right);
            default:
                return new VariableExpression(c);
        }
    }
}

시험

사용자의 입력 값에 따라 후위 연산으로 계산하는 App 클래스를 구현했습니다. 1에서 26까지의 숫자를 문자(a에서 z까지)로 해석하고 계산하도록 구현되었습니다.

public class App {
    public static void main(String() args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        Map<Character, Integer> map = new HashMap<>();
        System.out.println("알파벳 소문자와 '+', '-' 만 입력해주세요.");
        String input = br.readLine();

        for (char c: input.toCharArray()) {
            if (c != '+' && c != '-') {
                map.put(c, c - 96);
            }
        }

        for (Map.Entry<Character, Integer> entrySet: map.entrySet()) {
            System.out.println(entrySet.getKey() + " : " + entrySet.getValue());
        }
        // String 입력값을 넣는다.
        PostFixExpression expression = PostFixParser.parse(input);

        int res = expression.interpret(map);
        System.out.println(res);
    }
}



정리하다

  • 일반적으로 사용되는 패턴은 아니지만 2진수, 10진수 등 자주 분석해야 하는 프로그램 같은 경우 유용할 수 있습니다.
  • 가장 좋은 예는 Java 소스 코드를 JVM이 이해할 수 있도록 바이트 코드로 변환하는 Java 컴파일러입니다.
  • Java 및 Spring의 Expression Language의 정규식도 있습니다.