引子:你不懂老闆的心#

老闆難得找小菜談話,誇獎了一句「在公司表現格外出色,要繼續好好努力」,又評價同事梅星是個「普通員工」。

大鳥揭密職場潛台詞:

  • 老闆私下大誇某員工 → 多半意味著「最近有更多的任務需要你完成」
  • 老闆說某員工「普通」→ 其實是說這員工不夠聰明、能力不足

「要是有一個翻譯機或解釋器就好了」——這正好對應解釋器模式

解釋器模式#

解釋器模式(Interpreter Pattern):給定一個語言,定義它的文法的一種表示,並定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子。[DP]

何時使用:

  • 一種特定類型的問題發生頻率足夠高,值得將該問題的各個實例表述為一個簡單語言中的句子
  • 構建一個解釋器,透過解釋這些句子來解決該問題[DP]

經典應用:

  • 正規表達式(Regular Expression):與其為每個比對需求都寫一個演算法函式,不如使用通用的正規表達式來解釋執行
  • 瀏覽器:解釋 HTML 文法,把標記轉成網頁
  • 機器人控制:用「向前走 10 步、左轉 90 度」這樣的語言取代呼叫具體方法

結構#

  • AbstractExpression(抽象表達式):宣告一個抽象的解釋操作
  • TerminalExpression(終結符表達式):實作與文法中終結符相關聯的解釋操作;文法中每個終結符都有一個對應的具體終結表達式
  • NonterminalExpression(非終結符表達式):為文法中的非終結符實作解釋操作;遞迴呼叫各符號的實例變數
  • Context:包含解釋器之外的一些全域資訊
classDiagram
    class Client
    class Context
    class AbstractExpression {
        <<abstract>>
        +Interpret(Context)*
    }
    class TerminalExpression
    class NonterminalExpression
    Client --> AbstractExpression
    Client --> Context
    AbstractExpression <|-- TerminalExpression
    AbstractExpression <|-- NonterminalExpression
    NonterminalExpression o--> AbstractExpression
abstract class AbstractExpression
{
    public abstract void Interpret(Context context);
}

class TerminalExpression : AbstractExpression { /* ... */ }
class NonterminalExpression : AbstractExpression { /* ... */ }

範例:迷你音樂解釋器#

模擬 QBASIC 的 PLAY 語句。文法規則:

  • O 表示音階:O 1 低音、O 2 中音、O 3 高音
  • P 表示休止符
  • C D E F G A B 表示 Do-Re-Mi-Fa-So-La-Ti
  • 數字表示節拍(1 = 一拍、0.5 = 半拍……)
  • 所有字母與數字都用半角空格分開

例如「上海灘」第一句「浪奔」可寫成:O 2 E 0.5 G 0.5 A 3

演奏內容(Context)#

class PlayContext
{
    public string PlayText { get; set; }
}

抽象表達式#

abstract class Expression
{
    public void Interpret(PlayContext context)
    {
        if (context.PlayText.Length == 0) return;

        string playKey = context.PlayText.Substring(0, 1);
        context.PlayText = context.PlayText.Substring(2);

        double playValue = Convert.ToDouble(
            context.PlayText.Substring(0, context.PlayText.IndexOf(" ")));
        context.PlayText = context.PlayText.Substring(context.PlayText.IndexOf(" ") + 1);

        Excute(playKey, playValue);
    }

    public abstract void Excute(string key, double value);
}

終結符:音符與音階#

class Note : Expression
{
    public override void Excute(string key, double value)
    {
        string note = key switch
        {
            "C" => "1", "D" => "2", "E" => "3", "F" => "4",
            "G" => "5", "A" => "6", "B" => "7",
            _   => ""
        };
        Console.Write($"{note} ");
    }
}

class Scale : Expression
{
    public override void Excute(string key, double value)
    {
        string scale = (int)value switch
        {
            1 => "低音", 2 => "中音", 3 => "高音",
            _ => ""
        };
        Console.Write($"{scale} ");
    }
}

客戶端#

PlayContext context = new PlayContext();
context.PlayText = "O 2 E 0.5 G 0.5 A 3 E 0.5 G 0.5 D 3 ...";

Expression expression = null;
while (context.PlayText.Length > 0)
{
    string str = context.PlayText.Substring(0, 1);
    switch (str)
    {
        case "O":
            expression = new Scale();
            break;
        case "C": case "D": case "E": case "F":
        case "G": case "A": case "B": case "P":
            expression = new Note();
            break;
    }
    expression.Interpret(context);
}

擴展:新增「音速」文法#

要增加 T 1000(每節拍一秒)這樣的速度設置:

  • 加一個 Speed 表達式類
  • 在客戶端 switch 加一個 case "T"
class Speed : Expression
{
    public override void Excute(string key, double value)
    {
        string speed = value < 500 ? "快速"
                     : value >= 1000 ? "慢速"
                     : "中速";
        Console.Write($"{speed} ");
    }
}

客戶端的 switch 還可進一步用簡單工廠 + 反射重構,達到不改客戶端就能擴展的目的(見第 15 章)。

解釋器模式的優缺點#

優點#

  • 容易改變和擴展文法——使用類別表示文法規則,可用繼承擴展
  • 容易實作文法——抽象語法樹中各節點類的實作大體類似

說白了,解釋器模式就是「用迷你語言來表現程式要解決的問題,以迷你語言寫成迷你程式」。

缺點#

解釋器模式為文法中的每一條規則至少定義了一個類——包含許多規則的文法可能難以管理和維護。

當文法非常複雜時,建議改用其他技術(語法分析程式或編譯器生成器)。