C++ 設計模式 - 解釋器模式 Interpreter Pattern

大家有沒有想過當你在終端機輸入指令時,電腦是如何理解並執行這些指令的?或者當你使用正則表達式搜尋文字時,背後的機制是什麼?這些看似神奇的功能背後,其實都隱藏著一個強大而優雅的設計模式,那就是解釋器模式 Interpreter Pattern,今天就來聊聊這個模式,幫助我們理解當電腦「讀懂」我們的指令時背後的魔法。

什麼是解釋器模式?

解釋器模式是一種行為型設計模式,主要用來解析語言、處理簡單語法規則的。這個模式背後的想法其實很簡單:我們定義一套「語法規則」,然後讓每個「字元」或「符號」有自己專屬的解釋方式。這樣我們就能依照語法,逐步分析出我們想要的結果。

打個比方,就像我們學習一種新語言時,會先學習單字,接著組合句子,最後可以用來進行溝通。解釋器模式就像是這個過程的數位版,先定義每個「單字」的意思,再教你如何解釋這些字。

解釋器模式在計算機中的應用

計算機應該是最能體現解釋器模式的例子之一。想像你有一個簡單的計算機,輸入表達式如「2 + 3 * 4」,然後計算出結果。解釋器模式就可以幫助我們將這些數字與運算子號組合成一個能夠「理解」的語法樹。

首先先定義一個 Expression 介面,它會有一個 interpret() 方法。這個方法負責對數字和符號進行解釋,

1
2
3
4
5
6
// 抽象表達式
class Expression {
public:
virtual int interpret() = 0;
virtual ~Expression() = default;
};

接著定義具體的 NumberExpression(數字表達式)和 OperatorExpression(運算子表達式)。數字表達式會簡單地回傳它自己的值,而運算子表達式則會對兩個子表達式進行計算,比如加法、乘法等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 數字表達式
class NumberExpression : public Expression {
int number;
public:
NumberExpression(int num) : number(num) {}
int interpret() override {
return number;
}
};

// 加法表達式
class AddExpression : public Expression {
Expression* left;
Expression* right;
public:
AddExpression(Expression* l, Expression* r) : left(l), right(r) {}
int interpret() override {
return left->interpret() + right->interpret();
}
};

// 乘法表達式
class MultiplyExpression : public Expression {
Expression* left;
Expression* right;
public:
MultiplyExpression(Expression* l, Expression* r) : left(l), right(r) {}
int interpret() override {
return left->interpret() * right->interpret();
}
};

在前面已經定義了基礎的 NumberExpression、AddExpression 和 MultiplyExpression 類別。接下來我們在客戶端來實際使用這些表達式,讓整個流程完整起來。

我們會有一個解析器來遍歷表達式,並一步步解釋每個部分。假設我們要處理一個簡單的表達式「2 + 3」,客戶端的邏輯會像下面這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main() {
// 建立一個表示數字2的NumberExpression
Expression* num1 = new NumberExpression(2);

// 建立一個表示數字3的NumberExpression
Expression* num2 = new NumberExpression(3);

// 建立一個表示加法的AddExpression,將兩個數字作為參數傳入
Expression* addExpr = new AddExpression(num1, num2);

int result = addExpr->interpret();
std::cout << "2 + 3 = " << result << std::endl;

Expression* mulExpr = new MultiplyExpression(num1, num2);
result = mulExpr->interpret();
std::cout << "2 * 3 = " << result << std::endl;

// 釋放記憶體
delete num1;
delete num2;
delete addExpr;
delete mulExpr;

return 0;
}

執行上述程式碼,我們會得到以下輸出,當我們將表達式「2 + 3」傳給計算機時,解釋器就會根據我們定義的語法規則一步步處理,最終給出結果。

1
2
2 + 3 = 5
2 * 3 = 6

在這個範例中,客戶端的角色就是負責將表達式拼接在一起,然後使用解釋器模式的物件來完成運算。這樣任何新加入的運算子或表達式,只需要擴展 Expression 類別,而不用改動客戶端的邏輯,保持程式的靈活性和可維護性。

解釋器模式的優缺點

說到解釋器模式的優點,它最大的魅力就在於可以輕鬆地擴展和修改語法規則。比如我們可以在不改變原有架構的情況下,輕鬆加入新的運算子號或功能,這讓解釋器模式在處理語法解析時非常靈活。

解釋器模式的缺點也很明顯。當語法規則變得越來越複雜時,類別的數量會激增,這可能導致程式碼的維護和理解變得困難。另外解釋器模式的效能並不高,尤其是當處理大規模語法時,效能的瓶頸會更加明顯。

總結

解釋器模式就像一個小型語法解析器,適合用來處理簡單的語法規則,像是計算機這樣的場景。它的擴展性讓我們可以在程式中輕鬆加入更多規則,但同時也要留意其效能和維護性。對於那些需要頻繁變更規則、處理複雜語法的情況,解釋器模式提供了一個簡潔且靈活的解決方案。

現在使用解釋器模式的情境比以前少了很多。主要是因為解釋器模式通常用在處理自定義的語法或簡單的語言規則上,而現在有很多成熟的解析工具和函式庫可以直接使用,例如正則表達式、Lex、Yacc 等等,這些工具提供了更強大、效能更高的解決方案,因此不再需要自己從頭設計和實現一個語法解析系統。

解釋器模式的缺點是當語法規則變得複雜時,維護成本會上升,因此現代系統往往採用專用解析器或虛擬機器(例如 Java 虛擬機器或 JavaScript 引擎),因此手動實現解釋器模式的必要性變少了。

儘管如此,解釋器模式在某些特定情境中仍有用武之地,特別是在需要處理簡單且可擴展的語法時,或者需要快速實現自定義的 DSL(Domain-Specific Language)時,它仍是一個不錯的選擇。