設計模式(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#的工程化實現及擴充套件》