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