인터프리터 패턴(Interpreter Pattern)

  • 인터프리터 패턴은 간단한 언어의 문법을 정의하고 해석하는 패턴입니다.
  • 데이터를 주고/받을 때 데이터의 패턴(문법)을 객체화하여 언어를 해석합니다.
  • 언어의 패턴을 다루는 예제는 매우 많습니다.
  • 간단한 후휘 표기법 문법을 객체화하여 살펴보겠습니다.
    • 후위 표기법: 1 + 11 1 +와 같은 형태로 작성하는 방법, 연산자가 피연산자들 뒤에 위치합니다.

인터프리터 패턴 구조

  • 인터프리터 패턴이 문장의 어휘를 해석하고 처리하기 위해 다음과 같은 5가지 구성 요소가 필요합니다.
    • Context
      • 문장을 저장하고 관리하는 클래스
    • Abstract Expression
      • 문장을 해석하기 위한 해석자 인터페이스 클래스
    • Terminal Expression(Concrete Expression)
      • 문장의 종료를 나타내는 해석자 클래스 구현체
    • Non-Terminal Expression(Concrete Expression)
      • 문장의 비종료를 나타내는 해석자 클래스 구현체
    • Client

인터프리터 패턴: Context 클래스

  • Context 클래스는 문장을 저장하고 관리하는 클래스입니다.
  • 예제를 위해 {{ 1 1 + }}와 같은 문자열을 후위 표기법 문법으로 해석하겠습니다.
  • 문자열의 구성은 다음과 같습니다.
    • {{: 문자열의 시작을 의미합니다.
    • }}: 문자열의 종료를 의미합니다.
    • : 문자열 내부 정보는 공백으로 구분합니다.
  • Context 클래스는 문자열의 정보를 저장하고 관리하는 클래스로 토큰화가 필요합니다.
  • 토큰화 코드가 들어가있어 다소 복잡합니다.
class Context
{
public:
    Context(std::string data)
    {
        m_idx = 0;

        // Tokenize
        std::stringstream ss(data);
        const char delimiter = ' '; // space
        std::string token;
        while (std::getline(ss, token, delimiter))
        {
            m_token.push_back(token);
        }

        // print
        printf("==Print Token List==\n");
        for (int i = 0; i < m_token.size(); i++)
        {
            printf("[%d] -> %s\n", i, m_token[i]);
        }
        printf("==End Token List==\n");
    }

    bool isStart()
    {
        if (m_token[m_idx] == "{{")
        {
            m_idx++;
            return true;
        }
        else
        {
            return false;
        }
    }

    std::string next()
    {
        if (m_idx < m_token.size())
        {
            std::string token = m_token[m_idx];
            m_idx++;
            return token;
        }
        else
        {
            return "";
        }
    }

protected:
    std::vector<std::string> m_token;
    int m_idx;
};

인터프리터 패턴: Abstract Expression 인터페이스 클래스

  • 문장의 해석을 위한 인터페이스 클래스입니다.
  • 필요한 기능은 하위 클래스에서 구현합니다.
class Expression
{
public:
    virtual std::string interpret() = 0;
};

인터프리터 패턴: Terminal Expression 클래스 구현

  • Terminal 클래스는 문법 해석의 마지막으로, 문자 또는 숫자로 치환되지 않는 최종적인 기호입니다.
  • 여기서는 단순한 문자열을 입력받고, 반환하는 형태로 구현하였습니다.
// Terminal (Concrete Expression)
class Terminal :
    public Expression
{
public:
    Terminal(std::string n)
    {
        m_n = n;
    }

    std::string interpret()
    {
        return m_n;
    }

private:
    std::string m_n;
};

인터프리터 패턴: Non-Terminal Expression 클래스 구현

  • 문자열 해석을 위해 전개되는 표현을 담당합니다.
  • 문자열을 숫자로 치환하고, 더하기 연산을 수행한 다음 이를 다시 문자열로 반환하는 코드가 내부적으로 존재합니다.
  • 하나의 클래스에서 역할이 두 개로 구분되기 때문에 분리하여 작성할까 하다가 패턴의 의미를 강조하기 위해 하나로 구성했습니다.
// Non-Terminal (Concrete Expression)
class Add :
    public Expression
{
public:
    Add(std::string left, std::string right)
    {
        m_left = std::make_shared<Terminal>(left);
        m_right = std::make_shared<Terminal>(right);
    }

    std::string interpret() override
    {
        int left = std::atoi(m_left->interpret().c_str());
        int right = std::atoi(m_right->interpret().c_str());

        return std::to_string(left + right);
    }

private:
    std::shared_ptr<Expression> m_left;
    std::shared_ptr<Expression> m_right;
};

인터프리터 패턴: 실행 코드

  • 인터프리터 패턴은 다른 패턴에 비해 실행 코드(Client)가 다소 복잡합니다.
  • 생성한 문법에 따라 클라이언트의 구성이 달라지기 때문입니다.
  • 예제를 위해 간단한 문법 규칙을 사용하였지만, 문법 규칙이 많아지면 패턴이 복잡해집니다.
int main(const int argc, const char* argv[])
{
    std::shared_ptr<Context> context = std::make_shared<Context>("{{ 1 1 + }}");
    std::stack<std::string> stack;

    // Client
    while (true)
    {
        std::string token = context->next();
        if (token.empty())
        {
            break;
        }

        if (token == "}}")
        {
            std::cout << "End!" << std::endl;
        }
        else if (std::atoi(token.c_str()))
        {
            std::cout << "Stack Push" << std::endl;
            stack.push(token);
        }
        else if (token == "+")
        {
            std::cout << "+ Operator" << std::endl;

            std::string left = stack.top();
            stack.pop();
            std::string right = stack.top();
            stack.pop();

            std::shared_ptr<Add> add = std::make_shared<Add>(left, right);
            std::string value = add->interpret();
            std::cout << "Result: " << value << std::endl;
            stack.push(value);
        }
    }

    std::cout << "Final Result: " << stack.top() << std::endl;
    stack.pop();

    return 0;
}
  • 실행 결과
==Print Token List==
[0] -> {{
[1] -> 1
[2] -> 1
[3] -> +
[4] -> }}
==End Token List==
Stack Push
Stack Push
+ Operator
Result: 2
End!
Final Result: 2