解釋器模式 Interpreter 行爲型 設計模式(十九)

 
解釋器模式(Interpreter)
 
image_5c108d54_680
考慮上圖中計算器的例子
設計能夠用於計算加減運算(簡單起見,省略乘除),你會怎麼作? 
 
你可能會定義一個工具類,工具類中有N多靜態方法
好比定義了兩個方法用於計算a+b 和 a+b-c
public static int add(int a,int b){
return a+b;
}

public static int add(int a,int b,int c){
return a+b-c;
}
可是很明顯,若是形式有限,那麼能夠針對對應的形式進行編程
若是形勢變化很是多,這就不符合要求,由於加法和減法運算,兩個運算符與數值能夠有無窮種組合方式
好比 a+b+c+d+e+f、a-b-c+d、a-b+c....等等 
用有限的方法參數列表組合的形式,怎麼可能表達出無窮的變化?
 
也能夠經過函數式接口,可以提升必定的靈活性
package function;

@FunctionalInterface
public interface Function1<A,B,C,D,E,F,G, R> {
R xxxxx(A a,B b,C c,D d,E e,F f,G g);
}
image_5c108d54_4451
好處是能夠動態的自定義方程式,可是你可能須要定義不少函數式接口
並且,有限的函數式接口也不能解決無限種可能的
上面的方式都是以有限去應對無限,必然有行不通的時候
顯然,你須要一種翻譯識別機器,可以解析由數字以及+ - 符號構成的合法的運算序列
若是把運算符和數字都看做節點的話,可以逐個節點的進行讀取解析運算
這就是解釋器模式的思惟
解釋器不限定具體的格式,僅僅限定語法,可以識別遵循這種語法的「語言」書寫的句子
不固定你的形式,也就是不存在強制爲a+b的情形,可是你必須遵循固定語法,數字+ - 符號組成
Java編譯器能夠識別遵循java語法的表達式和語句,C語言編譯器能夠識別遵循C語言語法的表達式和語句。說的就是這個意思
 

意圖

給定一個語言,定義他的文法的一種表示,並定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。
解釋器模式其實就是編譯原理的思惟方式
 
若是某種特定類型的問題發生的頻率很高,那麼就能夠考慮將該問題的各個實例表述爲一個簡單語言中的句子,經過解釋器進行識別。
經典的案例就是正則表達式
咱們在實際開發中,常常須要判斷郵箱地址、手機號碼是否正確,若是沒有正則表達式
咱們須要編寫特定的算法函數進行判斷,去實現這些規則,好比一個算法可能用來判斷是不是郵箱,好比要求必須有@符號
 
正則表達式是用來解決字符串匹配的問題,他是解釋器模式思惟的一個運用實例
經過定義正則表達式的語法結構,進而經過表達式定義待匹配字符的集合,而後經過通用的算法來解釋執行正則表達式
解釋器模式將語法規則抽象出來,設置通用的語法規則,而後使用通用算法執行
使用正則表達式你不在須要本身手動實現算法去實現規則,你只須要按照正則表達式的語法,對你須要匹配的字符集合進行描述便可
有現成的通用算法來幫你實現,而語法相對於算法的實現,天然是簡單了不少
再好比瀏覽器解析HTML,咱們知道HTML頁面是由固定的元素組成的,有他的語法結構
可是一個HTML頁面的標籤的個數以及標籤內容的組合形式倒是變幻無窮的,可是瀏覽器能夠正確的將他們解析呈現出來
這也是一種解釋器的模型
 
在解釋器模式中,咱們須要 將待解決的問題,提取出規則,抽象爲一種「語言」
好比加減法運算,規則爲:有數值和+- 符號組成的合法序列
加減法運算就不能有乘除,不然就不符合語法
「1+2+3」就是這種語言的一個句子
 
好比遙控汽車的操做按鈕,規則爲:由前進、後退、左轉、右轉四種指令組成
遙控汽車就不能有起飛,不然就是不符合語法的
「前進 左轉 後退 前進 後退」就是這種語言的一個句子
 
解釋器就是要解析出來語句的含義
既然須要將待解決的問題場景提取出規則,那麼 如何描述規則呢?
 

語法規則描述

對於語法規則的定義,也有一套規範用於描述
Backus-Naur符號(就是衆所周知的BNF或Backus-Naur Form)是描述語言的形式化的數學方法
叫作範式,此後又有擴展的,叫作EBNF
範式基本規則
::= 表示定義,由什麼推導出
尖括號 < > 內爲必選項;
方括號 [ ] 內爲可選項;
大括號 { } 內爲可重複0至無數次的項;
圓括號 ( ) 內的全部項爲一組,用來控制表達式的優先級
豎線 | 表示或,左右的其中一個
引號內爲字符自己,引號外爲語法(好比 'for'表示關鍵字for )
 
有了規則咱們就能夠對語法進行描述,這是解釋器模式的基礎工做
好比加減法運算能夠這樣定義
expression:=value | plus | minus
plus:=expression ‘+’ expression
minus:=expression ‘-’ expression
value:=integer
值的類型爲整型數
有加法規則和減法規則
表達式能夠是一個值,也能夠是一個plus或者minus
而plus和minus又是由表達式結合運算符構成
 
能夠看得出來,有遞歸嵌套的概念
 

抽象語法樹

除了使用文法規則來定義規則,還能夠經過抽象語法樹的圖形方式直觀的表示語言的構成
文法規則描述了全部的場景,全部條件匹配的都是符合的,不匹配的都是不符合的
符合語法規則的一個「句子」就是語言規則的一個實例
抽象語法樹正是對於這個實例的一個描述
一顆抽象語法樹對應着語言規則的一個實例
 
關於抽象語法樹百科中這樣介紹
在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫爲 AST),或者語法樹(syntax tree)
是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。
樹上的每一個節點都表示源代碼中的一種結構。
之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。
 
好比 1+2+3+4-5是一個實例
image_5c108d54_58bf
因此說文法規則用於描述語言規則,抽象語法樹描述描述語言的一個實例,也就是一個「句子」

結構

image_5c108d54_6382
抽象表達式角色AbstractExpression
聲明一個抽象的解釋操做,全部的具體表達式操做都須要實現的抽象接口
接口主要是interpret()方法,叫作解釋操做
終結符表達式角色TerminalExpression
這是一個具體角色,實現與文法中的終結符相關聯的解釋操做,主要就是interpret()方法
一個句子中的每一個終結符都須要此類的一個實例
非終結符表達式NoneTerminalExpression
這也是一個具體的角色,對文法中的每一條規則R::=R1R2.....Rn都須要一個NoneTerminalExpression 類,注意是類,而不是實例
對每個R1R2...Rn中的符號都持有一個靜態類型爲AbstractExpression的實例變量;
實現解釋操做,主要就是interpret()方法
解釋操做以遞歸的方式調用上面所提到的表明R1R2...Rn中的各個符號的實例變量
上下文角色Context
包含解釋器以外的一些全局信息,通常狀況下都會須要這個角色
Client
構建表示該文法定義的語言中的一個特定的句子的抽象語法樹
抽象語法樹由NoneTerminalExpression 和 TerminalExpression的實例組裝而成
調用解釋器的interpret()方法

終結符和非終結符

通俗的說就是 不能單獨出如今推導式左邊的符號,也就是說終結符不能再進行推導,也就是終結符不能被別人定義
除了終結符就是非終結符
從抽象語法樹中能夠發現, 葉子節點就是終結符 除了葉子節點就是非終結符

角色示例解析

回到剛纔的例子
expression:=value | plus | minus
plus:=expression ‘+’ expression
minus:=expression ‘-’ expression
value:=integer 
 
上面是咱們給加減法運算定義的語法規則,由四條規則組成
其中規則value:=integer 表示的就是終結符
因此這是一個TerminalExpression,每個數字1+2+3+4-5中的1,2,3,4,5就是TerminalExpression的一個實例對象。
 
對於plus和minus規則,他們不是非終結符,屬於NoneTerminalExpression
他們的推導規則分別是經過‘+’和‘-’鏈接兩個expression
也就是角色中說到的「對文法中的每一條規則R::=R1R2.....Rn都須要一個NoneTerminalExpression 類」
也就是說plus表示一條規則,須要一個NoneTerminalExpression類
minus表示一條規則,須要一個NoneTerminalExpression類
expression是value 或者 plus 或者 minus,因此不須要NoneTerminalExpression類了
 
非終結符由終結符推導而來
NoneTerminalExpression類由TerminalExpression組合而成
因此須要:抽象表達式角色AbstractExpression
 
在計算過程當中,通常須要全局變量保存變量數據
這就是Context角色的通常做用
 
 
image_5c108d54_15ff
 
 
以最初的加減法爲例,咱們的句子就是數字和+ - 符號組成
好比 1+2+3+4-5
 
抽象角色AbstractExpression
package interpret;
public abstract class AbstractExpression {
public abstract int interpret();
}
終結符表達式角色TerminalExpression
內部有一個int類型的value,經過構造方法設置值
package interpret;

public class Value extends AbstractExpression {

private int value;
Value(int value){
this.value = value;
}

@Override
public int interpret() {
return value;
}
}
加法NoneTerminalExpression
package interpret;

public class Plus extends AbstractExpression {

private AbstractExpression left;
private AbstractExpression right;

Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}


@Override
public int interpret() {
return left.interpret() + right.interpret();
}
}
減法 NoneTerminalExpression
package interpret;

public class Minus extends AbstractExpression {

private AbstractExpression left;
private AbstractExpression right;

Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret() {
return left.interpret() - right.interpret();
}
}
客戶端角色
package interpret;

public class Client {

public static void main(String[] args) {

AbstractExpression expression = new Minus(
new Plus(new Plus(new Plus(new Value(1), new Value(2)), new Value(3)), new Value(4)),
new Value(5));
System.out.println(expression.interpret());
}
}
image_5c108d54_4566
 
上面的示例中,完成了解釋器模式的基本使用
咱們經過不斷重複的new 對象的形式,嵌套的構造了一顆抽象語法樹
只須要執行interpret 方法便可獲取最終的結果
 
這就是解釋器模式的基本原理
非終結符表達式由終結符表達式組合而來,也就是由非終結符表達式嵌套
嵌套就意味着遞歸,相似下面的方法,除非是終結符表達式,不然會一直遞歸
int f(int x) {
if (1 == x) {
return x;
} else {
return x+f(x-1);
}
}
上面的示例中,每次使用時,都須要藉助於new 按照抽象語法樹的形式建立一堆對象
好比計算1+2與3+4
是否是能夠轉換爲公式的形式呢?
也就是僅僅定義一次表達式,不論是1+2 仍是3+4仍是6+8 均可以計算?
因此咱們考慮增長「變量」這一終結符表達式節點
增長變量類Variable  終結符節點
內部包含名稱和值,提供值變動的方法
package interpret;
public class Variable extends AbstractExpression{
    private String name;
    private Integer value;
    Variable(String name,Integer value){
        this.name = name;
        this.value = value;
    }
    public void setValue(Integer value) {
        this.value = value;
    }
    @Override
    public int interpret() {
        return value;
    }
}
package interpret;
public class Client {
public static void main(String[] args) {
        //定義變量X和Y,初始值都爲0
        Variable variableX = new Variable("x", 0);
        Variable variableY = new Variable("y", 0);
        //計算公式爲: X+Y+X-1
        AbstractExpression expression2 = new Minus(new Plus(new Plus(variableX, variableY), variableX),
        new Value(1));
        variableX.setValue(1);
        variableY.setValue(3);
        System.out.println(expression2.interpret());
        variableX.setValue(5);
        variableY.setValue(6);
        System.out.println(expression2.interpret());
    }
}
image_5c108d54_354f
 
有了變量類 Variable,就能夠藉助於變量進行公式的計算
並且,很顯然, 公式只須要設置一次,並且能夠動態設置
經過改變變量的值就能夠達到套用公式的目的
 
通常的作法並非直接將值設置在變量類裏面,變量只有一個名字,將節點全部的值設置到Context類中
Context的做用能夠經過示例代碼感覺下
 

代碼示例

完整示例以下
image_5c108d54_4ce5
AbstractExpression抽象表達式角色 接受參數Context,若有須要能夠從全局空間中獲取數據
package interpret.refactor;

public abstract class AbstractExpression {
public abstract int interpret(Context ctx);
}
數值類Value 終結符表達式節點
內部還有int value
他不須要從全局空間獲取數據,因此interpret方法中的Context用不到
增長了toString方法,用於呈現 數值類的toString方法直接回顯數值的值
package interpret.refactor;

public class Value extends AbstractExpression {

private int value;

Value(int value) {
this.value = value;
}

@Override
public int interpret(Context ctx) {
return value;
}

@Override
public String toString() {
return new Integer(value).toString();
}
}
變量類Variable  終結符表達式
變量類擁有名字,使用內部的String name
變量類的真值保存在Context中,Context是藉助於hashMap存儲的
Context定義的類型爲Map<Variable, Integer>
因此,咱們重寫了equals以及hashCode方法
Variable的值存儲在Context這一全局環境中,值也是從中獲取
package interpret.refactor;

public class Variable extends AbstractExpression {

private String name;
Variable(String name) {
this.name = name;
}


@Override
public int interpret(Context ctx) {
return ctx.getValue(this);
}

@Override
public boolean equals(Object obj) {
if (obj != null && obj instanceof Variable) {
return this.name.equals(
((Variable) obj).name);
}
return false;
}

@Override
public int hashCode() {
return this.toString().hashCode();
}

@Override
public String toString() {
return name;
}
}
加法跟原來差很少,interpret接受參數Context,若有須要從Context中讀取數據
package interpret.refactor;

public class Plus extends AbstractExpression {

private AbstractExpression left;

private AbstractExpression right;

Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret(Context ctx) {
return left.interpret(ctx) + right.interpret(ctx);
}

@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}
package interpret.refactor;

public class Minus extends AbstractExpression {

private AbstractExpression left;

private AbstractExpression right;

Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret(Context ctx) {
return left.interpret(ctx) - right.interpret(ctx);
}

@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}
環境類Context
內部包含一個 private Map<Variable, Integer> map,用於存儲變量數據信息
key爲Variable 提供設置和獲取方法
package interpret.refactor;

import java.util.HashMap;

import java.util.Map;

public class Context {

private Map<Variable, Integer> map = new HashMap<Variable, Integer>();

public void assign(Variable var, Integer value) {
map.put(var, new Integer(value));
}

public int getValue(Variable var) {
Integer value = map.get(var);
return value;
}
}
package interpret.refactor;


public class Client {

public static void main(String[] args) {

Context ctx = new Context();

Variable a = new Variable("a");
Variable b = new Variable("b");
Variable c = new Variable("c");
Variable d = new Variable("d");
Variable e = new Variable("e");
Value v = new Value(1);

ctx.assign(a, 1);
ctx.assign(b, 2);
ctx.assign(c, 3);
ctx.assign(d, 4);
ctx.assign(e, 5);

AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);

System.out.println(expression + "= " + expression.interpret(ctx));
}
}

 

image_5c108d54_6b15
上述客戶端測試代碼中,咱們定義了a,b,c,d,e 五個變量
經過Context賦值,初始化爲1,2,3,4,5
而後構造了公式,計算結果
後續只須要設置變量的值便可套用這一公式
若是須要變更公式就修改表達式,若是設置變量就直接改變值便可
這種模式就實現了真正的靈活自由,只要是加減法運算,必然可以運算
再也不須要固定的參數列表或者函數式接口,很是靈活 
 
另外對於抽象語法樹的生成,你也能夠轉變形式
好比下面我寫了一個簡單的方法用於將字符串轉換爲抽象語法樹的Expression
/**
   * 解析字符串,構造抽象語法樹 方法只是爲了理解:解釋器模式 方法默認輸入爲合法的字符串,沒有考慮算法優化、效率或者不合法字符串的異常狀況
   *
   * @param sInput 合法的加減法字符串 好比 1+2+3
   */
  public static AbstractExpression getAST(String sInput) {
    //接收字符串參數形如 "1+2-3"
    //將字符串解析到List valueAndSymbolList中存放
    List<String> valueAndSymbolList = new ArrayList<>();
    //先按照 加法符號 + 拆分爲數組,以每一個元素爲單位使用 +鏈接起來存入List
    //若是以+ 分割內部還有減法符號 - 內部以減法符號- 分割
    //最終的元素的形式爲 1,+,2,-,3
    String[] splitByPlus = sInput.split("\\+");
    for (int i = 0; i < splitByPlus.length; i++) {
      if (splitByPlus[i].indexOf("-") < 0) {
        valueAndSymbolList.add(splitByPlus[i]);
      } else {
        String[] splitByMinus = splitByPlus[i].split("\\-");
        for (int j = 0; j < splitByMinus.length; j++) {
          valueAndSymbolList.add(splitByMinus[j]);
          if (j != splitByMinus.length - 1) {
            valueAndSymbolList.add("-");
          }
        }
      }
      if (i != splitByPlus.length - 1) {
        valueAndSymbolList.add("+");
      }
    }
    //通過前面處理元素的形式爲 1,+,2,-,3
    //轉換爲抽象語法樹的形式
    AbstractExpression leftExpression = null;
    AbstractExpression rightExpression = null;
    int k = 0;
    while (k < valueAndSymbolList.size()) {
      if (!valueAndSymbolList.get(k).equals("+") && !valueAndSymbolList.get(k).equals("-")) {
        rightExpression = new Value(Integer.parseInt(valueAndSymbolList.get(k)));
        if (leftExpression == null) {
          leftExpression = rightExpression;
        }
      }
      k++;
      if (k < valueAndSymbolList.size()) {
        rightExpression = new Value(Integer.parseInt(valueAndSymbolList.get(k + 1)));
        if (valueAndSymbolList.get(k).equals("+")) {
          leftExpression = new Plus(leftExpression, rightExpression);
        } else if (valueAndSymbolList.get(k).equals("-")) {
          leftExpression = new Minus(leftExpression, rightExpression);
        }
        k++;
      }
    }
    return leftExpression;
  }
 
經過上面的這個方法,咱們就能夠直接解析字符串了
image_5c108d55_271c

總結

解釋器模式是用於解析一種「語言」,對於使用頻率較高的,模式、公式化的場景,能夠考慮使用解釋器模式。
好比正則表達式,將「匹配」這一語法,定義爲一種語言
瀏覽器對於HTML的解析,將HTML文檔的結構定義爲一種語言
咱們上面的例子,將加減運算規則定義爲一種語言
因此,使用解釋器模式要注意「 高頻」「 公式」「 格式」這幾個關鍵詞
 
解釋器模式將語法規則抽象的表述爲類
解釋器模式 爲自定義語言的設計和實現提供了一種解決方案,它用於定義一組文法規則並經過這組文法規則來解釋語言中的句子。
 
解釋器模式很是容易擴展,若是增長新的運算符,好比乘除,只須要增長新的非終結符表達式便可
改變和擴展語言的規則很是靈活
非終結符表達式是由終結符表達式構成,基本上須要藉助於嵌套,遞歸,因此代碼自己通常比較簡單
像咱們上面那樣, Plus和Minus 的代碼差別很小
 
若是語言比較複雜,顯然,就會須要定義大量的類來處理
解釋器模式中 大量的使用了遞歸嵌套,因此說它的 性能是頗有問題的,若是你的系統是性能敏感的,你就更要慎重的使用
 
聽說解釋器模式在實際的系統開發中使用得很是少,另外也有一些開源工具
Expression4J、MESP(Math Expression String Parser)、Jep
因此 不要本身實現
 
另外還須要注意的是,從咱們上面的示例代碼中能夠看得出來
解釋器模式的重點在於AbstractExpression、TerminalExpression、NoneTerminalExpression的提取抽象
也就是對於文法規則的映射轉換
而至於如何轉換爲抽象語法樹,這是客戶端的責任
咱們的示例中能夠經過new不斷地嵌套建立expression對象
也能夠經過方法解析抽象語法樹,均可以根據實際場景處理
簡言之, 解釋器模式不關注抽象語法樹的建立,僅僅關注解析處理
 
因此我的見解:
但凡你的問題場景能夠抽象爲一種語言,也就是有規則、公式,有套路就可使用解釋器模式
不過若是有替代方法,能不用就不用
若是非要用,你也不要本身寫
相關文章
相關標籤/搜索