設計模式: 直譯器模式 - 概念、實現

2020-08-09 12:17:46

學習設計模式不光要學習設計模式的思想,還要去深入理解,爲什麼要用這個設計模式。
如何深入理解?讀優秀的框架程式碼,看別人程式碼,瞭解它們的使用場景。 - - - 博主老師(感謝他)

本文介紹瞭直譯器模式的概念,實現

1、概念

定義:給定一個語言,定義它的文法的一種表示,並定義一個直譯器,該直譯器使用該表示來解釋語言中的句子。

概念有點難懂…下面 下麪會做具體介紹。

(1)什麼是文法
舉個例子:假設有如下短語

我是程式設計師
我是設計師
我是搬運工

上面的短語:「我」可以看作是主語,「是」則表示謂語,「程式設計師」、「設計師」、「搬運工」這些名詞可以看作是賓語,也就是說,上面的短語是「主謂賓」結構,這樣的結構我們稱爲一條文法。可以通過文法來造成更多符合該文法的句子。

文法不止是「主謂賓」、「定狀補」這種語法結構。我們也可以將上面的短語堪稱是一條「我是[名詞]」這樣的結構。這也是一條文法。

再舉個例子:假設有ab開頭ef結尾中間排列N(N>=0)個cd的字串

abcd…cdef

隨着N的不同,具體的字串也不同,我們可以得到「abef」、「abcdef」、「abcdcdef」之類的字串。
在電腦科學中,我們將上述「a」、「b」、「c」、「d」、「e」、「f」這6個字元稱爲一種形式語言的字元表,而字元組成的集合稱爲形式語言。定義符號S,從符號S出發推導上述字串,那麼就可以得到如下推導式:

S ::= abA*ef
A ::= cd

其中「::=」表示推導;符號「*」表示閉包,就是符號A可以有0個或N個重複。其中A、S稱爲非終結符號(能推導出右邊的式子);abcdef爲非終結符號(不能繼續推導)。這個推導式就是文法,並且我們將這種文法稱爲形式文法。形式文法與形式語言相對應,用來描述形式語言。

(2)什麼是直譯器
可以將直譯器理解成一個翻譯機,用來翻譯類似於「abef」、「abcdef」、「abcdcdef」之類的字串句子

再回過頭來看直譯器模式的定義:給定一個語言(如由abcdef六個字元組成的字串集合),定義它的文法的一種表示(如上面的S ::= abAef 和 A ::= cd),並定義一個直譯器,該直譯器使用該表示來解釋語言中的句子。
這樣似乎能理解了。在程式設計中,很少會用標準的文法推導式,很多時候直接使用類似於「A
B」這樣的字串規則表達式表示文法。

2、實現

直譯器模式一個比較常見的場景是對算數表達式的解釋
例如表達式"m + n + p", 如果使用直譯器模式對該表達式進行解釋,那麼代表數位的m、n和p三個字母我們就可以看成是終結符號,"+"這個算術運算子看成是非終結符號。

定義抽象的算數運算直譯器

public abstract class ArithmeticExpression {

    public abstract int interpret();
}

數位直譯器

public class NumExpression extends ArithmeticExpression {
    private int num;

    public NumExpression(int num) {
        this.num = num;
    }

    @Override
    public int interpret() {
        return num;
    }
}

抽象的操作直譯器(因爲可能會有加、減、乘、除等子類)

public abstract class OperatorExpression extends ArithmeticExpression {

    // 聲名兩個成員變數儲存運算子號兩邊的數位直譯器
    protected ArithmeticExpression exp1, exp2;

    public OperatorExpression(ArithmeticExpression exp1, ArithmeticExpression exp2) {
        this.exp1 = exp1;
        this.exp2 = exp2;
    }
}

加號直譯器

public class AdditionExpressioin extends OperatorExpression {

    public AdditionExpressioin(ArithmeticExpression exp1, ArithmeticExpression exp2) {
        super(exp1, exp2);
    }


    @Override
    public int interpret() {
        return exp1.interpret() + exp2.interpret();
    }
}

計算業務類

public class Calculator {

    private Stack<ArithmeticExpression> mExpStack = new Stack<>();

    public Calculator(String expression) {
        ArithmeticExpression exp1, exp2;

        String[] elements = expression.split(" ");

        for (int i = 0; i < elements.length; i++) {
            switch (elements[i].charAt(0)) {
                case '+':
                    exp1 = mExpStack.pop();
                    exp2 = new NumExpression(Integer.valueOf(elements[++i]));
                    mExpStack.push(new AdditionExpressioin(exp1, exp2));
                    break;
                default:
                    mExpStack.push(new NumExpression(Integer.valueOf(elements[i])));
                    break;
            }
        }
    }

    public int calculate() {
        return mExpStack.pop().interpret();
    }
}

測試

public class Test {

    public static void main(String[] args) {
        Calculator c = new Calculator("123 + 1 + 58 + 888");
        System.out.println(c.calculate());
    }
}

輸出

1070

上面我們就定義了個加法的運算。如果現在要支援減法,只需要新增一個減法直譯器

public class SubtractionExpression extends OperatorExpression {

    public SubtractionExpression(ArithmeticExpression exp1, ArithmeticExpression exp2) {
        super(exp1, exp2);
    }

    @Override
    public int interpret() {
        return exp1.interpret() - exp2.interpret();
    }
}

在業務類裏面加上case '-'的分支

case '-':
    exp1 = mExpStack.pop();
    exp2 = new NumExpression(Integer.valueOf(elements[++i]));
    mExpStack.push(new SubtractionExpression(exp1, exp2));
    break;

直譯器模式的靈活性非常強。但是如果要做混合運算(有乘除),就會涉及到優先順序,實現就會複雜的多。

3、小結:

感覺自己的開發中不怎麼會有能用到直譯器模式的場景。在jdk中的正則、Spring的EL表達式等這些都用到瞭直譯器模式,
還有mybatis對sql的解析等等。

適用場景:

  1. 重複發生的問題:
    例如,多個應用伺服器,每天產生大量的日誌,需要對日誌檔案進行分析處理,由於各個伺服器的日誌格式不同,但是數據要素是相同的,按照直譯器的說法就是終結符表達式都是相同的,但是非終結符表達式就需要制定了。在這種情況下,可以通過程式來一勞永逸地解決該問題。
  2. 簡單的語法需要j解釋的場景

何紅輝 關愛民 · Android原始碼設計模式解析於實戰 · 人民郵電出版社
https://www.az1314.cn/art/46