設計模式(15) 解釋器模式

項目中有時會遇到某類問題出現得非常頻繁,而且它們的變化也基本上以一些規律性的方式進行變化。對於這類問題,如果編寫一個對象類進行處理,隨着業務變更,將需要頻繁地修改代碼、編譯、部署。與其反覆做這種工作,不如把它們抽象爲一個語言(語法定義可能很簡單,也可能很複雜),這樣就可以極大地增加代碼的業務適應性。
正則表達式就是解釋器模式的一種應用;再比如,假設有這樣的業務場景 :部門經理可以審批員工的辦公用品申請,但如果某個申請單的金額大於1萬,那麼部門經理就沒有權限審批了。這個邏輯可以表示爲:

本部門員工(申請單) AND 申請單.金額小於10萬

類似的規則常常會發生更改,比如可能需要增加一條:如果員工本身是行政助理,他收集全部門辦公用品單,爲了簡化手續,每個部門的辦公用品可以由他一個人掛名申請,因此金額可以大於1萬,這時就需要修改這個表達式。所以在這類場景下,可以考慮增加一個能讀懂這個表達式的子系統,在犧牲一些效率的情況下,專門解釋執行類似的表達式。

解釋器模式

解釋器模式被用來解決單純堆疊類結構難於應付業務變化的問題。
GOF對解釋器模式的描述爲:
Given a language, define a represention for its grammar along with an interpreter that uses the representation to interpret sentences in the language..
— Design Patterns : Elements of Reusable Object-Oriented Software

代碼示例:
下面是利用解釋器模式實現的一個簡單的只支持加減法的計算器

public interface IExpression
{
    //解析公式和數值,其中var中的key-val是參數-具體數字
    int Interpreter(Dictionary<string, int> var);
}

public class VarExpression : IExpression
{
    private string key;
    public VarExpression(string key)
    {
        this.key = key;
    }

    public int Interpreter(Dictionary<string, int> var)
    {
        return var[this.key];
    }
}

public abstract class SymbolExpression : IExpression
{
    protected IExpression left;
    protected IExpression right;
    public SymbolExpression(IExpression left, IExpression right)
    {
        this.left = left;
        this.right = right;
    }

    public abstract int Interpreter(Dictionary<string, int> var);
}

//加法解析器
public class AddExpression : SymbolExpression
{
    public AddExpression(IExpression left, IExpression right) : base(left, right) { }
    public override int Interpreter(Dictionary<string, int> var)
    {
        return this.left.Interpreter(var) + this.right.Interpreter(var);
    }
}

//減法解析器
public class SubExpression : SymbolExpression
{
    public SubExpression(IExpression left, IExpression right) : base(left, right) { }
    public override int Interpreter(Dictionary<string, int> var)
    {
        return this.left.Interpreter(var) - this.right.Interpreter(var);
    }
}

public class Calculator
{
    private IExpression expression;

    public Calculator(string exp)
    {
        //定義一個棧,安排運算的先後順序
        Stack<IExpression> stack = new Stack<IExpression>();

        //表達式拆分爲字符數組
        char[] charArray = exp.ToCharArray();

        //構建表達式樹
        IExpression left = null;
        IExpression right = null;
        for (int i = 0; i < charArray.Length; i++)
        {
            switch (charArray[i])
            {
                case '+':
                    left = stack.Pop();
                    right = new VarExpression(charArray[++i].ToString());
                    stack.Push(new AddExpression(left, right));
                    break;
                case '-':
                    left = stack.Pop();
                    right = new VarExpression(charArray[++i].ToString());
                    stack.Push(new SubExpression(left, right));
                    break;
                default: //公式中的變量
                    stack.Push(new VarExpression(charArray[i].ToString()));
                    break;
            }
        }
        this.expression = stack.Pop();
    }

    public int Run(Dictionary<string, int> var)
    {
        return this.expression.Interpreter(var);
    }
}

調用:

string exp = "a+b-c";
Dictionary<string, int> var = new Dictionary<string, int>();
var.Add("a", 3);
var.Add("b", 5);
var.Add("c", 7);

Calculator calculator = new Calculator(exp);
Console.WriteLine(calculator.Run(var)); //結果=1

這裏有兩個關鍵點:自定義的語言和那個Context對象,它們是貫穿解釋器始終的對象,至於解釋器的骨架則是由一個個表達式對象完成的,解釋器的作用是把Context放進去,然後調度一個個表達式對象,直至完成整個語言的解釋過程。

UML類圖:

從UML類圖可知解釋器模式包含這幾個角色:

  • Context,環境角色,保存瞭解釋器運行需要的上下文;
  • AbstractExpression,抽象表達式,是所有計算表達式的抽象接口,表示當前表達式節點及其分支下所有節點,具體的解釋任務分別由TerminalExpression和NonTerminalExpression完成;
  • TerminalExpression,終結符表達式,示例中的VarExpression,實現與文法中的元素相關聯的解釋操作,通常一個解釋器模式中只有一個終結符表達式,但有多個實例,對應不同的終結符。
  • NonTerminalExpression,非終結符表達式,示例中的AddExpression和SubExpression,非終結符表達式根據邏輯的複雜程度而增加,原則上每個文法規則都對應一個非終結符表達式。

適用場景

  • 雖然相關操作頻繁出現,而且也有一定規律可循,但如果通過大量層次性的類來表示這種操作,設計上顯得比較複雜。
  • 執行上對效率的要求不是特別高,但對於靈活性的要求非常高。

優點

  • 可擴展性比較好
  • 增加了新的解釋表達式的方式。
  • 易於實現簡單文法。

缺點

  • 可利用場景比較少。
  • 對於複雜的文法比較難維護。
  • 解釋器模式會引起類膨脹。
  • 解釋器模式採用遞歸調用方法,性能較差。

解釋器是一個比較少用的模式,如果確實遇到“一種特定類型的問題發生的頻率足夠高”的情況,準備使用解釋器模式時,建議優先考慮一些成熟的第三方、開源的解析工具。

參考書籍:
王翔著 《設計模式——基於C#的工程化實現及擴展》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章