1. 程式人生 > >解析器模式(Interpreter Pattern)。

解析器模式(Interpreter Pattern)。

定義

直譯器模式是一種按照規定語法進行解析的方案,在現在專案中使用較少,其定義如下:

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

  • Expression——抽象直譯器

具體的解釋任務由各個實現類完成,具體的直譯器分別由TerminalExpression和NonterminalExpression完成。

  • TerminalExpression——終結符表示式

實現與文法中的元素相關聯的解釋操作,通常一個直譯器模式中只有一個終結符表示式,擔憂多個例項,對應不同的終結符。

  • NonterminalExpression——非終結符表示式

文法中每條規則對應於一個非終結表示式。非終結符表示式根據邏輯的複雜程度而增加,原則上每個文法規則都對應一個非終結符表示式。

  • Context——環境角色

例如採用HashMap。

通用原始碼

直譯器是一個比較少用的模式,以下為其通用原始碼,可以作為參考。抽象表示式通常只有一個方法,如下所示。

public abstract class Expression {
	/**
	 * 每個表示式必須有一個解析任務
	 * 
	 * @param ctx
	 * @return
	 */
	public abstract Object interpreter(Context ctx);
}

抽象表示式是生成語法集合(也叫做語法樹)的關鍵,每個語法集合完成指定語法解析任務,他是通過遞迴呼叫的方式,最終由最小的語法單元進行解析完成。終結符表示式如下所示。

public class TerminalExpression extends Expression {

	@Override
	public Object interpreter(Context ctx) {
		// 通常終結符表示式只有一個,但是有多個物件
		return null;
	}

}

通常,終結符表示式比較簡單,主要是處理場景元素和資料的轉換。

非終結符表示式如下所示。

public class NonterminalExpression extends Expression {
    /**
     * 每個非終結符表示式都會對其他表示式產生依賴
     *
     * @param expressions
     */
    public NonterminalExpression(Expression... expressions) {
    }

    @Override
    public Object interpreter(Context ctx) {
        // 進行文法處理
        return null;
    }

}

每個非終結符表示式都代表了一個文法規則,並且每個文法規則都只關心自己周邊的文法規則的結果(注意是結果),因此這就產生了每個非終結符表示式呼叫自己周邊的非終結符表示式,然後最終、最小的文法規則就是終結符表示式,終結符表示式的概念就是如此,不能夠再參與比自己更小的文法運算了。

客戶類如下所示。

public class Client {
	public static void main(String[] args) {
		Context ctx =new Context();
		// 通常定一個語法容器,容納一個具體的表示式,通常為ListArray、LinkedList、Stack等型別
		Stack<Expression> stack = new Stack<Expression>();
		/*
		 * 進行語法判斷,併產生遞迴呼叫
		 */
		// 產生一個完整的語法樹,由各個具體的語法分析進行解析
		Expression exp = stack.pop();
		// 具體元素進入場景
		exp.interpreter(ctx);
	}
}

通常Client是一個封裝類,封裝的結果就是傳遞進來一個規範語法檔案,解析器分析後產生結果並返回,避免了呼叫者與語法解析器的耦合關係。

優點

直譯器是一個簡單語法分析工具,他最顯著的優點就是擴充套件性,修改語法規則只要修改相應的非終結符表示式就可以了,若擴充套件語法,則只要增加非終結符類就可以了。

缺點

  • 解析器模式會引起類膨脹

每個語法都要產生一個非終結符表示式,語法規則比較複雜時,就可能產生大量的類檔案,為維護帶來了非常多的麻煩。

  • 直譯器模式採用遞迴呼叫方法

每個非終結符表示式只關心與自己有關的表示式,每個表示式需要知道最終的結果,必須一層一層的剝繭,無論是面向過程的語言還是面向物件的語言,遞迴都是在必要條件下使用的,他導致除錯非常複雜。想想看,如果要排查一個語法錯誤,我們是不是要一個斷點一個斷點的除錯下去,直到最小的語法單元。

  • 效率問題

直譯器模式由於使用了大量的迴圈和遞迴,效率是一個不容忽視的問題,特別是一用於解析複雜、冗長的語法時,效率是難以忍受的。

使用場景

  • 重複發生的問題可以使用直譯器模式

例如,多個引用伺服器,每天產生大量的日誌,需要對日誌檔案進行分析處理,由於各個伺服器的日誌格式不同,但是資料要素是相同的,按照直譯器的說法就是終結符表示式都是相同的,但是非終結符表示式就需要制定了。在這種情況下,可以通過程式來一勞永逸的解決該問題。

  • 一個簡單語法需要解釋的場景

為什麼是簡單?看看非終結符表示式,文法規則越多,複雜度越過,而且類間還要進行遞迴呼叫。想想看,多個類之間的呼叫你需要什麼樣的耐心和資訊去排查問題。因此,直譯器模式一般用來解析比較標準的字符集,例如SQL語法分析,不過該部分逐漸被專用工具所取代。

在某些特用的商業環境下才會採用直譯器模式,而且現在模型運算的例子非常多,目前很多商業機構已經能夠提供出大量的資料進行分析。

注意事項

儘量不要在重要的模組中使用直譯器模式,否則維護會是一個很大的問題。在專案中可以使用shell、JRuby、Groovy等指令碼語言來代替直譯器模式,彌補Java編譯型語言的不足。我們子啊一個銀行的分析型專案中就採用JRuby進行運算處理,避免使用直譯器模式的四則運算,效率和效能各方面表現良好。

最佳實踐

直譯器模式在實際的系統開發中使用得非常少,因為他會引起效率、效能以及維護等問題,一般在大中型的框架型專案能夠找到他的身影,如一些資料分析工具,報表設計工具、科學計算工具等,若你確定遇到“一種特定型別的額問題發生的頻率足夠高”的情況,準備使用直譯器模式時,可以考慮一下Expression4J、MESP(Math Expression String Parser)、Jep等開源的解析工具包,功能都異常強大,而且非常容易使用,效率也還不錯,實現大多數的數學運算完全沒有問題,自己沒有必要從頭開始編寫直譯器。有人已經建立了一條康莊大道,何必再走自己的泥濘小路呢?