Hướng dẫn Java Design Pattern – Interpreter

1. Interpreter Pattern là gì?

Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.

Interpreter Pattern là một trong những Pattern thuộc nhóm hành vi (Behavior Pattern).

Interpreter nghĩa là thông dịch, mẫu này nói rằng “để xác định một biểu diễn ngữ pháp của một ngôn ngữ cụ thể, cùng với một thông dịch viên sử dụng biểu diễn này để diễn dịch các câu trong ngôn ngữ”.

Nói cho dễ hiểu, Interpreter Pattern giúp người lập trình có thể “xây dựng” những đối tượng “động” bằng cách đọc mô tả về đối tượng rồi sau đó “xây dựng” đối tượng đúng theo mô tả đó.

Metadata (mô tả) –> [Interpreter Pattern] –> Đối tượng tương ứng.

Interpreter Pattern có hạn chế về phạm vi áp dụng. Mẫu này thường được sử dụng để định nghĩa bộ ngữ pháp đơn giản (grammar), trong các công cụ quy tắc đơn giản (rule), …

2. Cài đặt Interpreter Pattern như thế nào?

Các thành phần tham gia mẫu Interpreter:

  • Context : là phần chứa thông tin biểu diễn mẫu chúng ta cần xây dựng.
  • Expression : là một interface hoặc abstract class, định nghĩa phương thức interpreter chung cho tất cả các node trong cấu trúc cây phân tích ngữ pháp. Expression được biểu diễn như một cấu trúc cây phân cấp, mỗi implement của Expression có thể gọi một node.
  • TerminalExpression (biểu thức đầu cuối): cài đặt các phương thức của Expression, là những biểu thức có thể được diễn giải trong một đối tượng duy nhất, chứa các xử lý logic để đưa thông tin của context thành đối tượng cụ thể.
  • NonTerminalExpression (biểu thức không đầu cuối): cài đặt các phương thức của Expression, biểu thức này chứa một hoặc nhiều biểu thức khác nhau, mỗi biểu thức có thể là biểu thức đầu cuối hoặc không phải là biểu thức đầu cuối. Khi một phương thức interpret() của lớp biểu thức không phải là đầu cuối được gọi, nó sẽ gọi đệ quy đến tất cả các biểu thức khác mà nó đang giữ.
  • Client : đại diện cho người dùng sử dụng lớp Interpreter Pattern. Client sẽ xây dựng cây biểu thức đại diện cho các lệnh được thực thi, gọi phương thức interpreter() của node trên cùng trong cây, có thể truyền context để thực thi tất cả các lệnh trong cây.

2.1. Ví dụ Interpreter Pattern trong ứng dụng calculator theo ngôn ngữ tự nhiên

Trong ví dụ bên dưới chúng ta sẽ xây dựng ứng dụng calculator theo ngôn ngữ tự nhiên. Ví dụ: 20 cộng 8 = 28 hay 10 trừ 4 = 6

Chương trình của chúng ta như sau:

InterpreterEngineContext.java

package com.maixuanviet.patterns.behavioral.interpreter.math;
 
public class InterpreterEngineContext {
 
    public int add(String input) {
        String[] tokens = interpret(input);
        int num1 = Integer.parseInt(tokens[0]);
        int num2 = Integer.parseInt(tokens[1]);
        return (num1 + num2);
    }
 
    public int subtract(String input) {
        String[] tokens = interpret(input);
        int num1 = Integer.parseInt(tokens[0]);
        int num2 = Integer.parseInt(tokens[1]);
        return (num1 - num2);
    }
 
    private String[] interpret(String input) {
        String str = input.replaceAll("[^0-9]", " ");
        str = str.replaceAll("( )+", " ").trim();
        return str.split(" ");
    }
}

Expression.java

package com.maixuanviet.patterns.behavioral.interpreter.math;
 
public interface Expression {
    int interpret(InterpreterEngineContext context);
}

AddExpression.java

package com.maixuanviet.patterns.behavioral.interpreter.math;
 
public class AddExpression implements Expression {
 
    private String expression;
 
    public AddExpression(String expression) {
        this.expression = expression;
    }
 
    @Override
    public int interpret(InterpreterEngineContext context) {
        return context.add(expression);
    }
}

SubtractExpression.java

package com.maixuanviet.patterns.behavioral.interpreter.math;
 
public class SubtractExpression implements Expression {
 
    private String expression;
 
    public SubtractExpression(String expression) {
        this.expression = expression;
    }
 
    @Override
    public int interpret(InterpreterEngineContext context) {
        return context.subtract(expression);
    }
}

Client.java

package com.maixuanviet.patterns.behavioral.interpreter.math;
 
public class Client {
 
    public static void main(String args[]) {
        System.out.println("20 cộng 8 = " + interpret("20 cộng 8"));
        System.out.println("10 trừ 4 = " + interpret("10 trừ 4"));
    }
 
    private static int interpret(String input) {
        Expression exp = null;
        if (input.contains("cộng")) {
            exp = new AddExpression(input);
        } else if (input.contains("trừ")) {
            exp = new SubtractExpression(input);
        } else {
            throw new UnsupportedOperationException();
        }
        return exp.interpret(new InterpreterEngineContext());
    }
}

Output của chương trình:

20 cộng 8 = 28
10 trừ 4 = 6

2.2. Ví dụ sử dụng Interpreter để chuyển chữ cái La Mã sang số thập phân

Có bốn nguyên tắc cơ bản để đọc và viết chữ số La Mã:

  • Chữ số La Mã được thể hiện bằng chữ cái của bảng chữ cái: I=1;  V=5;  X=10;  L=50;  C=100;  D=500;  M=1000.
  • Một chữ cái có thể lặp lại giá trị của nó nhiều lần, tối đa ba lần. Ví dụ: XXX = 30, CC = 200,…
  • Nếu một hoặc nhiều chữ cái được đặt sau một chữ cái có giá trị lớn hơn, cộng số trước đó. Ví dụ:
    • VI = 6 (5 + 1 = 6)
    • LXX = 70 (50 + 10 + 10 = 70)
    • MCC = 1200 (1000 + 100 + 100 = 1200)
  • Nếu một chữ cái được đặt trước một chữ cái có giá trị lớn hơn, trừ đi số trước đó. Ví dụ:
    • IV = 4 (5 – 1 = 4)
    • XC = 90 (100 – 10 = 90)
    • CM = 900 (1000 – 100 = 900)

Chi tiết về chữ cái La Mã, các bạn xem thêm ở các link bên dưới:

Chương trình của chúng ta như sau:

Context.java

package com.maixuanviet.patterns.behavioral.interpreter.roman;
 
public class Context {
    private String input;
    private int output;
 
    public Context(String input) {
        this.input = input;
    }
 
    public void setInput(String input) {
        this.input = input;
    }
 
    public String getInput() {
        return input;
    }
 
    public int getOutput() {
        return output;
    }
 
    public void setOutput(int output) {
        this.output = output;
    }
}

Expression.java

package com.maixuanviet.patterns.behavioral.interpreter.roman;
 
public abstract class Expression {
 
    public void interpret(Context context) {
        if (context.getInput().length() == 0) {
            return;
        }
         
        if (context.getInput().startsWith(nine())) {
            context.setOutput(context.getOutput() + 9 * multiplier());
            context.setInput(context.getInput().substring(2));
        } else if (context.getInput().startsWith(four())) {
            context.setOutput(context.getOutput() + 4 * multiplier());
            context.setInput(context.getInput().substring(2));
        } else if (context.getInput().startsWith(five())) {
            context.setOutput(context.getOutput() + 5 * multiplier());
            context.setInput(context.getInput().substring(1));
        }
 
        while (context.getInput().startsWith(one())) {
            context.setOutput(context.getOutput() + 1 * multiplier());
            context.setInput(context.getInput().substring(1));
        }
    }
 
    public abstract String one();
 
    public abstract String four();
 
    public abstract String five();
 
    public abstract String nine();
 
    public abstract int multiplier();
}

ThousandExpression.java

package com.maixuanviet.patterns.behavioral.interpreter.roman;
 
public class ThousandExpression extends Expression {
 
    @Override
    public String one() {
        return "M";
    }
 
    @Override
    public String four() {
        return " ";
    }
 
    @Override
    public String five() {
        return " ";
    }
 
    @Override
    public String nine() {
        return " ";
    }
 
    @Override
    public int multiplier() {
        return 1000;
    }
}

HundredExpression.java

package com.maixuanviet.patterns.behavioral.interpreter.roman;
 
public class HundredExpression extends Expression {
 
    @Override
    public String one() {
        return "C";
    }
 
    @Override
    public String four() {
        return "CD";
    }
 
    @Override
    public String five() {
        return "D";
    }
 
    @Override
    public String nine() {
        return "CM";
    }
 
    @Override
    public int multiplier() {
        return 100;
    }
}

TenExpression.java

package com.maixuanviet.patterns.behavioral.interpreter.roman;
 
public class TenExpression extends Expression {
 
    @Override
    public String one() {
        return "X";
    }
 
    @Override
    public String four() {
        return "XL";
    }
 
    @Override
    public String five() {
        return "L";
    }
 
    @Override
    public String nine() {
        return "XC";
    }
 
    @Override
    public int multiplier() {
        return 10;
    }
 
}

OneExpression.java

package com.maixuanviet.patterns.behavioral.interpreter.roman;
 
public class OneExpression extends Expression {
 
    @Override
    public String one() {
        return "I";
    }
 
    @Override
    public String four() {
        return "IV";
    }
 
    @Override
    public String five() {
        return "V";
    }
 
    @Override
    public String nine() {
        return "IX";
    }
 
    @Override
    public int multiplier() {
        return 1;
    }
}

Client.java

package com.maixuanviet.patterns.behavioral.interpreter.roman;
 
import java.util.ArrayList;
import java.util.List;
 
public class Client {
 
    public static void main(String[] args) {
        String[] romans = { "IV", "XII", "CLIX", "MMXVIII", "MMMDLIV" };
        for (String roman : romans) {
            convertRomanToNumber(roman);
        }
    }
 
    private static void convertRomanToNumber(String roman) {
        List<Expression> tree = new ArrayList<>();
        tree.add(new ThousandExpression());
        tree.add(new HundredExpression());
        tree.add(new TenExpression());
        tree.add(new OneExpression());
 
        Context context = new Context(roman);
        for (Expression exp : tree) {
            exp.interpret(context);
        }
        System.out.println(roman + " = " + context.getOutput());
    }
}

Output của chương trình:

IV = 4
XII = 12
CLIX = 159
MMXVIII = 2018
MMMDLIV = 3554

3. Lợi ích của Interpreter Pattern là gì?

  • Dễ dàng thay đổi và mở rộng ngữ pháp. Vì mẫu này sử dụng các lớp để biểu diễn các quy tắc ngữ pháp, chúng ta có thể sử dụng thừa kế để thay đổi hoặc mở rộng ngữ pháp. Các biểu thức hiện tại có thể được sửa đổi theo từng bước và các biểu thức mới có thể được định nghĩa lại các thay đổi trên các biểu thức cũ.
  • Cài đặt và sử dụng ngữ pháp rất đơn giản. Các lớp xác định các nút trong cây cú pháp có các implement tương tự. Các lớp này dễ viết và các phân cấp con của chúng có thể được tự động hóa bằng trình biên dịch hoặc trình tạo trình phân tích cú pháp.

4. Sử dụng Interpreter Pattern khi nào?

Interpreter Pattern được sử dụng hiệu quả khi:

  • Bộ ngữ pháp đơn giản. Pattern này cần xác định ít nhất một lớp cho mỗi quy tắc trong ngữ pháp. Do đó ngữ pháp có chứa nhiều quy tắc có thể khó quản lý và bảo trì.
  • Không quan tâm nhiều về hiệu suất. Do bộ ngữ pháp được phân tích trong cấu trúc phân cấp (cây) nên hiệu suất không được đảm bảo.

Interpreter Pattern thường được sử dụng trong trình biên dịch (compiler), định nghĩa các bộ ngữ pháp, rule, trình phân tích SQL, XML, …