定义

给定一个语言,定义他的语法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子



图纸

设计模式——2_2 解释器(Interpreter)-LMLPHP



一个例子:怎么计算一串字符组成的算式?

正如他的名字一样,解释器模式真的就是一种适合用于解释一种语言的文法的设计模式

这段话看不懂?没事,我也看不懂

抽象的看不懂,所以我们举个具体的例子,让我们开始吧:


直接输入的算式

假定现在有一个输入框,允许用户输入一个算式,由程序帮用户计算出结果

简单的来说就是,用户输入:1+2,一回车,显示3


那你会说,这也太简单了,不就是计算器吗?

真的这么简单?用户输入的方式可不是一个按钮一个按钮的点数字,而是直接输入一个 字符串。你的任务是解释这个字符串,然后计算出他表达的算式的结果。除非你是用类似JavaScript中的 eval 这种封装好的直接把字符串转换成表达式的“作弊器”,否则可需要一个思路


算式树

为了对用户输入的字符串进行计算,流程应该是这样的:
设计模式——2_2 解释器(Interpreter)-LMLPHP

  1. 验证字符串

    这个跟主体没啥关系,但是实际要做这个功能的话验证器对象+验证方法是必要的

  2. 解析字符串

    对字符串里面的内容进行分割、解析,这就是解释器要做的事情

  3. 算式对象

    用于表示解析后的算式,他应该有一个compute方法,可以用于获取计算结果

而这个算式对象,我们一般会用一个树状结构来表示他,比如这样:

1 + 2 ∗ 3 − 4 1+2*3-4 1+234
设计模式——2_2 解释器(Interpreter)-LMLPHP

我们在构筑这棵树的时候,运算符左侧的部分永远不变,只操作右侧的部分,+或-向上方延申,*或/向下方延申


这棵算式树的结果=root位置的节点的计算结果

root位置是➖,要得到她的计算结果,我需要先知道左侧的➕的计算结果,➕的计算结果需要知道✖的计算结果,所以自然而然的就会最先执行2✖3,之后1➕6,最后用7➖4得到3

于是,我们可以写出这样一个类:

/**
 * 式子树
 * 一个树状结构表示一个式子
 */
public class EquationTree {

	/**
	 * 根节点
	 */
	private Node root;
	/**
	 * 上次插入的节点
	 */
	private Node lastNode;

	/**
	 * 添加节点
	 */
	public void add(Object value) {
		Node node = new Node(value);

		if (root == null) {
			root = node;
		} else if (value instanceof Character) {
			//运算符节点
			switch ((Character) value) {
				case '+':
				case '-':
					//+、-直接在根元素上方插入
					node.setLeft(root);
					root = node;
					break;
				case '*':
				case '/':
					//*、/,找到lastNode(这时候一定是元素节点),取代他
					if (lastNode == root) {
						root = node;
					}else {
						lastNode.parent.setRight(node);
					}
					node.setLeft(lastNode);

					break;
			}
		} else {
			//运算元素节点
			//直接插入到lastNode(此时一定是运算符节点)的右侧
			lastNode.setRight(node);
		}

		lastNode = node;
	}

	@Override
	public String toString() {
		return "EquationTree=" + toString(root);
	}

	private String toString(Node node) {
		if (node == null) {
			return "";
		} else {
			return toString(node.left) + node.toString() + toString(node.right);
		}
	}

	public Double count() {
		if (root == null) {
			return null;
		} else {
			return count(root);
		}
	}

	private Double count(Node node) {
		if (node.value instanceof Character) {
			//运算符位置的结算 = 左侧运算结果 运算符 右侧运算结果
			switch ((Character) node.value) {
				case '+':
					return count(node.left) + count(node.right);
				case '-':
					return count(node.left) - count(node.right);
				case '*':
					return count(node.left) * count(node.right);
				case '/':
					return count(node.left) / count(node.right);
			}
		} else if (node.value instanceof EquationTree) {
			//子式子
			return ((EquationTree) node.value).count();
		} else {
			//数字
			return (Double) node.value;
		}

		throw new RuntimeException(node.value + " 存在异常");
	}

	/**
	 * 节点
	 */
	private static class Node {
		static int index = 0;
		int i = index++;
		Object value;

		Node parent, left, right;

		public Node(Object value) {
			this.value = value;
		}

		public void setLeft(Node left) {
			this.left = left;
			left.parent = this;
		}

		public void setRight(Node right) {
			this.right = right;
			right.parent = this;
		}

		@Override
		public String toString() {
			if (value instanceof EquationTree) {
				return "(" + value.toString() + ")";
			} else {
				return value.toString();
			}
		}
	}
}

我们定义了 EquationTree 用于表示一个算式树,并在里面定义了子类 Node,用来表示树上面的元素

Node 的值我们允许三种情况:

  1. Character类型:运算符(加减乘除)
  2. Double类型:数字
  3. EquationTree类型:子算式(如果用户输入的算式里面有括号,那就要把整个括号都看成一个元素,就会用到这个类型)

如何构成算式树

现在我们有了 EquationTree,也有了用户输入的 String,现在我们需要一个可以解释用户输入的String,把他转换成EquationTree的手柄,就像这样:

/**
 * 调用手柄
 */
public class Handler {

    /**
     * 把式子字符串转换成等式树
     */
    public EquationTree read(String context) { 
        //去除空格并转成char数组
        char[] cs = context.replace(" ", "").toCharArray();
        //组合成解释器用的上下文
        List<Character> characterList = new LinkedList<>();
        for (char c : cs) {
            characterList.add(c);
        }

        return InterpreterFactory.getEquationInterpreter().interpret(characterList);
    }
}

Handlerread 方法中,我们通过 Interpreter(解释器) 实现了将String转换成EquationTree,而这个解释器的结构,应该是这样的:
设计模式——2_2 解释器(Interpreter)-LMLPHP

InterpreterFactory(解释器工厂)
/**
 * 解释器工厂
 */
public class InterpreterFactory {

    private static final EquationInterpreter equationInterpreter = new EquationInterpreter(); 
    private static final NumberInterpreter numberInterpreter = new NumberInterpreter();
    private static final SymbolInterpreter symbolInterpreter = new SymbolInterpreter();

    public static EquationInterpreter getEquationInterpreter() {
        return equationInterpreter;
    }

    public static NumberInterpreter getNumberInterpreter() {
        return numberInterpreter;
    }

    public static SymbolInterpreter getSymbolInterpreter() {
        return symbolInterpreter;
    }
}
Interpreters
/**
 * 解释器
 */
public interface Interpreter<E> {

    E interpret(List<Character> context);
}

/**
 * 运算符解释器
 */
public class SymbolInterpreter implements Interpreter<Character> {

    @Override
    public Character interpret(List<Character> context) {
        return context.remove(0);
    }
}

/**
 * 数字解释器
 */
public class NumberInterpreter implements Interpreter<Double> {

    @Override
    public Double interpret(List<Character> context) {
        //第一位一定是数字的一部分,即使他不是数字也有可能是正负号
        StringBuilder numberBuilder = new StringBuilder();
        numberBuilder.append(context.remove(0));

        while (!context.isEmpty()) {
            //如果上下文中还有内容,则继续读取
            if (Character.isDigit(context.get(0))) {
                numberBuilder.append(context.remove(0));
            } else {
                break;//读取到非数字的部分,说明这段数字部分结束
            }
        }

        return Double.parseDouble(numberBuilder.toString());
    }
}

/**
 * 式子解析器
 */
public class EquationInterpreter implements Interpreter<EquationTree> {

    @Override
    public EquationTree interpret(List<Character> context) {
        EquationTree equationTree = new EquationTree();

        boolean isSymbol = false;//接下来是否是读取运算符
        while (!context.isEmpty()) {//还有内容就继续读取
            Character c = context.get(0);

            if (isSymbol) {
                //读取运算符
                equationTree.add(InterpreterFactory.getSymbolInterpreter().interpret(context));
            } else {
                //读取运算元素
                if (c.equals('(')) {
                    context.remove(0);
                    //读取子式子
                    equationTree.add(InterpreterFactory.getEquationInterpreter().interpret(subContext(context)));
                } else {
                    //读取数字
                    equationTree.add(InterpreterFactory.getNumberInterpreter().interpret(context));
                }
            }

            isSymbol = !isSymbol;
        }

        return equationTree;
    }

    private List<Character> subContext(List<Character> context) {
        List<Character> sub = new LinkedList<>();
        int leftCount = 1;
        while (!context.isEmpty()) {//还有内容就继续读取
            Character c = context.remove(0);
            if (c.equals(')')) {
                leftCount--;
                if (leftCount == 0) {
                    break;
                }
            } else {
                sub.add(c);
                if (c.equals('(')) {
                    leftCount++;
                }
            }
        }

        return sub;
    }
}

思路是这样的,在 Handler 中将我们要进行解析的文本转换成 字符列表,然后传给 EquationInterpreter(算式解释器),而 EquationInterpreter(算式解释器) 不会自己直接去把所有文本解释出来,而是读取 字符列表 里面的内容,判断接下来要读取的这部分到底是运算符、数字还是子算式(括号),再根据读取到的结果,决定最终是要调用 SymbolInterpreter(运算符解释器)NumberInterpreter(数字解释器) 还是对子算式调用 EquationInterpreter(算式解释器)

但是无论读取到什么信息,最终都会汇总到最外层的 EquationInterpreter(算式解释器) 的返回值中,再反馈给 Handler


我知道你懒得看上面那些代码,所以上面的流程画成图的画,大概长这样:
设计模式——2_2 解释器(Interpreter)-LMLPHP

既然都写到这里了,最后我们给他补个main方法,比如这样:

	public static void main(String[] args) { 
        while (true) {
            System.out.println("请输入一个等式(按c退出):");
            Scanner scanner = new Scanner(System.in);
            String question = scanner.nextLine();
            if (question.equals("c")) {
                break;
            }

            Handler handler = new Handler();
            EquationTree tree = handler.read(question);
            System.out.println(tree);
            System.out.println(tree.count());
        }
    }

输出:

设计模式——2_2 解释器(Interpreter)-LMLPHP


解释器模式在实际开发过程中运用极少,只在实现一些特定需求时会发挥作用。但是里面的设计思想值得玩味:这个逻辑如果一股脑放在一起去解析,虽然文法规则是固定的,但是依然显得很复杂,会有大量的if-else,需要你考虑各种情况。而在上述实现中,我们通过把不同的文法规则拆分到不同的解释器对象中,并用 EquationInterpreter(算式解释器) 对他们进行调用的统筹。这样一来,在每个负责不同区域的解释器对象中,我们的目的都很明确且简单,这是一种分治思想的体现。

而这正是一个标准的解释器实现



碎碎念

解释器和分治

分治这种思想在编程中几乎随处可见,他是指把一个复杂的问题分成多个的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并

比如刚开始学编程时就会接触到的递归求阶乘,排序算法中的归并排序、快速排序;又或者是树状图里面的各项操作,都不乏这种思想的身影


解释器和组合模式

在上例中,我们将算式用树状结构 EquationTree 的形式体现出来,而 EquationTree 对象本质上其实是多个 Node 的组合工具类。

是的,这里用到了组合模式。而且并不是巧合,解释器在解释文法的时候,多数情况下都会将被解释的文本转义成树状结构,而组合模式是对树状结构极好的实现方式


解释器和享元

解释器模式其中一个缺陷是由于所有的文法都需要通过对应的解释器来进行解释,所以在执行的过程中会产生大量的重复对象。

本例比较特殊,因为解释器没有状态,所以全都可以直接用单例。而当你无法对解释器使用单例的时候,享元是可以考虑的辅助模式之一,用她来管理解释器对象可以大大减少重复对象的产生




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

02-29 21:13