日志

软件设计模式学习(十九)解释器模式

 来源    2020-05-23    1  

解释器是一种不常使用的设计模式,它用于描述如何构成一个简单的语言解释器,主要应用于使用面向对象语言开发的编译器和解释器设计。当我们需要开发一个新的语言时,可以考虑使用解释器模式


模式动机

如果在系统中某一特定类型的问题发生的频率很高,此时可以考虑将这些问题的实例表述为一个语言中的句子。再构建一个解释器,解释器通过解释这些句子来解决对应的问题。

举个例子,我们希望系统提供一个功能来支持一种新的加减法表达式语言,当输入表达式为 "1 + 2 + 3 - 4 + 1" 时,输出计算结果为 3。为了实现上述功能,需要对输入表达式进行解释,如果不作解释,直接把 "1 + 2 + 3 - 4 + 1" 丢过去,现有的如 Java、C 之类的编程语言只会把它当作普通的字符串,不可能实现我们想要的计算效果。我们必须自己定义一套规则来实现该语句的解释,即实现一个简单语言来解释这些句子,这就是解释器模式的模式动机。


模式定义

定义语言的文法,并且建立一个解释器来解释该语言中的句子,这里的 “语言” 意思是使用规定格式和语法的代码,它是一种类行为型模式。


模式结构

  1. AbstractExpression(抽象表达式)

    声明了抽象的解释操作,它是所有终结符表达式和非终结符表达式的公共父类

  2. TerminalExpression(终结符表达式)

    抽象表达式的子类,实现了文法中的终结符相关联的解释操作,在句子中每一个终结符都是该类的一个实例。

  3. NonterminalExpression(非终结符表达式)

    也是抽象表达式的子类,实现了文法中的非终结符相关联的解释操作,非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归方式来完成。

  4. Context(环境类)

    环境类又称上下文类,它用于存储解释器之外的一些全局信息,通常它临时存储了需要解释的语句。

  5. Client(客户类)

    客户类中构造了表示以规定文法定义的一个特定句子的抽象语法树,该抽象语法树由非终结符表达式和终结符表达式实例组合而成。在客户类中还将调用解释操作,实现对句子的解释,有时候为了简化客户类代码,也可以将抽象语法树的构造工作封装到专门的类中完成,客户端只需提供待解释的句子并调用该类的解释操作即可,该类可以称为解释器封装类


模式分析

还是以之前提到的加减法表达式语言来举例,我们要为这门语言定义语法规则,可以使用如下文法来定义

expression ::= value | symbol
symbol ::= expression '+' expression | expression '-' expression
value ::= an integer	// 一个整数值

该文法规则包含三条定义语句,第一句是表达式的组成方式,expression 是我们最终要得到的句子,假设是 "1 + 2 + 3 - 4 + 1",那么该句的组成元素无非就是两种,数字(value)和运算符号(symbol),如果用专业术语来描述的话,symbol 和 value 称为语法构造成分或语法单位。根据句子定义,expression 要么是一个 value,要么是一个 symbol。

value 是一个终结符表达式,因为它的组成元素就是一个整数值,不能再进行分解。与之对应的 symbol 则是非终结符表达式,它的组成元素仍旧可以是表达式 expression,expression 又可以是 value 或者 symbol,即可以进一步分解。

按照上述的文法规则,我们可以通过一种称之为抽象语法树(Abstract Syntax Tree)的图形方式来直观地表示语言的构成,每一颗抽象语法树对应一个语言实例,如 "1 + 2 + 3 - 4 + 1" 可以通过如图的抽象语法树来表示。

每一个具体的语句都可以用类似的抽象语法树来表示,终结符表达式类的实例作为树的叶子节点,而非终结符表达式类的实例作为非叶子节点。抽象语法树描述了如何构成一个复杂的句子,通过对抽象语法树的分析,可以识别出语言中的终结符和非终结符类。

在解释器模式中,每一个终结符和非终结符都有一个具体类与之对应,正因为使用类来表示每一个语法规则,使得系统具有较好的扩展性和灵活性。对于所有的终结符和非终结符,首先要抽象出一个公共父类

public abstract class AbstractExpression {
    public abstract void interpret(Context ctx);
}

对于终结符表达式,其代码主要是对终结符元素的处理

public class TerminalExpression extends AbstractExpression {
    public void interpret(Context ctx) {
        // 对于终结符表达式的解释操作
    }
}

对于终结符表达式,其代码比较复杂,因为通过非终结符表达式可以将表达式组合成更复杂的结构。表达式可以通过非终结符连接在一起,对于两个操作元素的非终结符表达式,其典型代码如下

public class NonterminalExpression extends AbstractExpression {
    
    private AbstractExpression left;
    private AbstractExpression right;
    
    public NonterminalExpression(AbstractExpression left, AbstractExpression right) {
        this.left = left;
        this.right = right;
    }
    
    public void interpret(Context ctx) {
        // 递归调用每一个组成部分的 interpret() 方法
        // 在递归调用时指定组成部分的连接方式,即非终结符的功能
    }
}

通常在解释器模式中还提供了一个环境类 Context,用于存储一些全局信息,用于在进行具体的解释操作时从中获取相关信息。当系统无须提供全局公共信息时,可以省略环境类

public class Context {
    
    private HashMap map = new HashMap();
    
    public void assign(String key, String value) {
        // 往环境类中设值
    }
    public void lookup(String key) {
        // 获取存储在环境类中的值
    }
}

模式实例

现需构造一个语言解释器,使系统可以执行整数间的乘、除和求模运算。当用户输入表达式 "3 * 4 / 2 % 4",输出结果为 2

  1. 抽象表达式类 Node(抽象节点)

    public interface Node {
        public int interpret();
    }
  2. 终结符表达式类 ValueNode(值节点类)

    public class ValueNode implements Node {
    
        private int value;
    
        public ValueNode(int value) {
            this.value = value;
        }
    
        @Override
        public int interpret() {
            return this.value;
        }
    }
  3. 抽象非终结符表达式类 SymbolNode(符号节点类)

    public abstract class SymbolNode implements Node {
    
        protected Node left;
        protected Node right;
    
        public SymbolNode(Node left, Node right) {
            this.left = left;
            this.right = right;
        }
    }
  4. 非终结符表达式类 MulNode(乘法节点类)

    public class MulNode extends SymbolNode {
    
        public MulNode(Node left, Node right) {
            super(left, right);
        }
    
        @Override
        public int interpret() {
            return super.left.interpret() * super.right.interpret();
        }
    }
  5. 非终结符表达式类 DivNode(除法节点类)

    public class DivNode extends SymbolNode {
    
        public DivNode(Node left, Node right) {
            super(left, right);
        }
    
        @Override
        public int interpret() {
            return super.left.interpret() / super.right.interpret();
        }
    }
  6. 非终结符表达式类 ModNode(求模节点类)

    public class ModNode extends SymbolNode {
    
        public ModNode(Node left, Node right) {
            super(left, right);
        }
    
        @Override
        public int interpret() {
            return super.left.interpret() % super.right.interpret();
        }
    }
  7. 解释器封装类 Calculator(计算器类)

    Calculator 类是本实例的核心类之一,Calculator 类中定义了如何构造一棵抽象语法树,在构造过程中使用了栈结构 Stack。通过一连串判断语句判断字符,如果是数字,实例化终结符表达式类 ValueNode 并压栈;如果判断为运算符号,则取出栈顶内容作为其左表达式,而将之后输入的数字封装在 ValueNode 类型的对象作为其右表达式,创建非终结符表达式 MulNode 类型的对象,最后将该表达式压栈。

    public class Calculator {
    
        private String statement;
        private Node node;
    
        public void build(String statement) {
    
            Node left = null, right = null;
            Stack<Node> stack = new Stack<Node>();
    
            String[] statementArr = statement.split(" ");
    
            for (int i = 0; i < statementArr.length; i++) {
                if (statementArr[i].equalsIgnoreCase(("*"))) {
                    left = stack.pop();
                    int val = Integer.parseInt(statementArr[++i]);
                    right = new ValueNode(val);
                    stack.push(new MulNode(left, right));
                } else if (statementArr[i].equalsIgnoreCase(("/"))) {
                    left = stack.pop();
                    int val = Integer.parseInt(statementArr[++i]);
                    right = new ValueNode(val);
                    stack.push(new DivNode(left, right));
                } else if (statementArr[i].equalsIgnoreCase(("%"))) {
                    left = stack.pop();
                    int val = Integer.parseInt(statementArr[++i]);
                    right = new ValueNode(val);
                    stack.push(new ModNode(left, right));
                } else {
                    stack.push(new ValueNode(Integer.parseInt(statementArr[i])));
                }
            }
            this.node = stack.pop();
        }
    
        public int compute() {
            return node.interpret();
        }
    }
  8. 客户端测试类 Client

    程序执行时将递归调用每一个表达式类的 interpret() 的解释方法,最终完成对整棵抽象语法树的解释。

    public class Client {
    
        public static void main(String[] args) {
            String statement = "3 * 4 / 2 % 4";
            Calculator calculator = new Calculator();
            calculator.build(statement);
            int result = calculator.compute();
            System.out.println(statement + " = " + result);
        }
    }
  9. 运行结果


模式优缺点

解释器模式优点如下:

  1. 易于改变和扩展文法。由于使用类来表示语言的文法规则,可以通过继承机制来改变或扩展文法。
  2. 易于实现文法。抽象语法树中每一个节点类的实现方式都是相似的,编写并不复杂。
  3. 增加了新的解释表达式的方式。增加新的表达式时无须对现有表达式类进行修改,符合开闭原则

解释器模式缺点如下:

  1. 对于复杂文法难以维护。
  2. 执行效率低。解释器模式使用了大量循环和递归调用。
  3. 应用场景有限。

软件设计模式学习(十八)命令模式


相关文章
学习设计模式第十九 - 迭代器模式
日志本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考.转载请标明原作者TerryLee.部分示例代码来自DoFactory. 概述 在面向对象的软件设计 ...
学习设计模式第十八 - 解释器模式
日志部分示例代码来自DoFactory. 概述 如果一个特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子.这样就可以构建一个解释器,该解释器通过解释这些句子来解决 ...
C#设计模式之十九策略模式(Stragety Pattern)行为型
日志原文:C#设计模式之十九策略模式(Stragety Pattern)[行为型]一.引言   今天我们开始讲"行为型"设计模式的第七个模式,该模式是[策略模式],英文名称是:Stra ...
1
javascript设计模式学习之九——命令模式
日志一.命令模式使用场景及定义 命令模式常见的使用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接受者是谁,也不知道请求的具体操作是什么.此时希望用一种松耦合的方式来设计程序,使得请求的发送者和 ...
1
解释器模式 Interpreter 行为型 设计模式(十九)
日志解释器模式(Interpreter) 考虑上图中计算器的例子 设计可以用于计算加减运算(简单起见,省略乘除),你会怎么做?  你可能会定义一个工具类,工具类中有N多静态方法 比如定义了两个方法用于计算 ...
1
设计模式学习总结(十九)--命令模式
日志定义 将请求封装成对象,以便使用不同的请求.日志.队列等来参数化其他对象.命令模式也支持撤销操作. 角色 Command:定义命令的统一接口 ConcreteCommand:Command接口的实现者 ...
1
设计模式学习总结(十六)--解释器模式
日志定义 解释器模式(Interpreter),给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子. 角色 AbstractExpression: 抽象表达式. ...
1
学习设计模式第二十六 - 访问者模式
日志示例代码来自DoFactory. 概述 当你想要为一个对象的组合增加新的能力,且封装并不重要时,就使用访问者模式. 意图 表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素的类的前提下 ...
1
学习设计模式第二十五 - 模板方法模式
日志本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考.转载请标明原作者TerryLee.部分示例代码来自DoFactory. 概述 变化一直以来都是软件 ...
学习设计模式第二十四 - 策略模式
日志示例代码来自DoFactory. 概述 策略模式通过将实现相同功能的不同方法封装起来,使调用者可以方便的在需要的方法之间做出选择. 意图 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换 ...
1
学习设计模式第二十 - 中介者模式
日志示例代码来自DoFactory. 概述 中介者模式出现的背景是,尽管将一个系统分割成许多对象通常可以增加其复用性,但是对象间相互连接的激增又会降低其可复用性.大量的连接使得一个对象不可能在没有其他对象 ...
1
学习设计模式第十五 - 代理模式
日志本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考.转载请标明原作者TerryLee.部分示例代码来自DoFactory. 概述 在软件系统中,有些对 ...
1
Java进阶篇设计模式之九----- 解释器模式和迭代器模式
日志前言 在上一篇中我们学习了行为型模式的责任链模式(Chain of Responsibility Pattern)和命令模式(Command Pattern).本篇则来学习下行为型模式的两个模式, 解 ...
6
Java设计模式之九 ----- 解释器模式和迭代器模式
日志前言 在上一篇中我们学习了行为型模式的责任链模式(Chain of Responsibility Pattern)和命令模式(Command Pattern).本篇则来学习下行为型模式的两个模式, 解 ...
云计算设计模式(十九)——执行重构模式
日志云计算设计模式(十九)--执行重构模式 设计应用程序,使得它能够在不须要又一次部署或者又一次启动应用程序又一次配置.这有助于保持可用性并降低停机时间. 背景和问题 一个主要目的为重要的应用.如商业和企 ...
1
设计模式(十九):访问者模式
日志优点: ① 可以在不改变各元素的类的前提下,定义作用于这些元素的新操作新状态,将元素和作用于元素之上的操作之间的耦合解脱开,使得操作可以相对集中和自由变化增减. ② 对于元素操作或状态,具有良好的灵活 ...
1
设计模式学习总结(九)--享元模式
日志定义 享元模式就是运行共享技术有效地支持大量细粒度对象的复用.系统使用少量对象,而且这些都比较相似,状态变化小,可以实现对象的多次复用.享元的目的是为了减少不会要额内存消耗,将多个对同一对象的访问集中 ...
设计模式(十九)—— 备忘录模式
日志模式简介 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可将该对象恢复到原先保存的状态. 很多时候我们要记录一个对象的内部状态,为了允许用户撤销不确定的操作或从 ...
1
设计模式(十九):备忘录模式
日志一.定义 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样就可以将该对象恢复到原先保存的状态 二.实例 2.1 发起人:记录当前时刻的内部状态,负责定义哪些属于备份范围 ...
设计模式笔记之十五 (解释器模式)
日志解释器模式 解释器模式就是定义一种语言,并定义这个语言的解释器,解释器能够按照定义好的语法来将这种语言‘翻译’成使用者能理解的语言. 广泛上来讲,Java是一种定义的语言,JVM就是一种‘解释器’,而 ...
1