報錯:hadoop There appears to be a gap in the edit log. We expected txid 927, but got txid 1265.
直譯器模式
案例
張三公司最近需要開發一款簡單的加法/減法直譯器,只要輸入一個加法/減法表示式,它就能夠計算出表示式結果,當輸入字串表示式為“1+2+3+4-5”時,將輸出計算結果為3。很快張三就寫了出來:
1.計算表示式類:
public class Calculator { public int calculate(String expression) { String[] expressionArray = expression.split(""); Stack<Integer> stack = new Stack<>(); for (int i = 0; i < expressionArray.length; i++) { if ("+".equals(expressionArray[i])) { // 如果是 + 號,則再取下一個數累加後放入棧中 Integer num = stack.pop(); stack.push(num + Integer.valueOf(expressionArray[++i])); } else if ("-".equals(expressionArray[i])) { // 如果是 - 號,也是再取下一個數相減後放入棧中 Integer num = stack.pop(); stack.push(num - Integer.valueOf(expressionArray[++i])); } else { // 數字直接放入棧中 stack.push(Integer.valueOf(expressionArray[i])); } } return stack.pop(); } }
2.客戶端使用:
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
String expression = "1+2+3+4-5";
System.out.println(expression + "=" +calculator.calculate(expression));
}
}
3.使用結果:
1+2+3+4-5=5
這裡的例子比較簡單,主要目的是為了介紹下面的直譯器模式,它主要就是使用面嚮物件語言構成一個簡單的語法直譯器,如這裡的"1+2+3+4-5",在 Java 中是不能直接解釋執行的,必須自己定義一套文法規則來實現對這些語句的解釋,相當於設計一個自定義語言。就像編譯原理中語言和文法、詞法分析、語法分析等過程。
模式介紹
一種行為型設計模式。定義了一個直譯器,來解釋給定語言和文法的句子。其實質是把語言中的每個符號定義成一個(物件)類,從而把每個程式轉換成一個具體的物件樹。
角色構成
- AbstractExpression(抽象表示式):在抽象表示式中聲明瞭抽象的解釋操作,它是所有終結符表示式和非終結符表示式的公共父類。
- TerminalExpression(終結符表示式):終結符表示式是抽象表示式的子類,它實現了與文法中的終結符相關聯的解釋操作,在句子中的每一個終結符都是該類的一個例項。通常在一個直譯器模式中只有少數幾個終結符表示式類,它們的例項可以通過非終結符表示式組成較為複雜的句子。
- NonterminalExpression(非終結符表示式):非終結符表示式也是抽象表示式的子類,它實現了文法中非終結符的解釋操作,由於在非終結符表示式中可以包含終結符表示式,也可以繼續包含非終結符表示式,因此其解釋操作一般通過遞迴的方式來完成。
- Context(環境類):環境類又稱為上下文類,它用於儲存直譯器之外的一些全域性資訊,通常它臨時儲存了需要解釋的語句。
UML 類圖
直譯器模式是一種使用頻率相對較低但學習難度較大的設計模式,它用於描述如何使用面嚮物件語言構成一個簡單的語言直譯器。就是說在特定的應用場景中,可以建立一種新的語言,這種語言擁有自己的表示式和結構,即文法規則,這些問題的例項將對應為該語言中的句子。在這裡的案例中"1+2+3+4-5",可以用如下文法規則來定義:
expression ::= value | operation
operation ::= expression '+' expression | expression '-' expression
value ::= an integer
首先這裡的符號"::="表示“定義為”的意思。第一行表示的是一個表示式,表示式的組成方式為 value 和 operation,即操作和數字組成。第二行 operation 操作表示表示式相加或表示式相減。第三行就是指 value 是一個數字。下面就通過直譯器模式來實現加法/減法直譯器的功能。
程式碼改造
在直譯器模式中,每一種終結符和非終結符都有一個具體類與之對應,對於所有的終結符和非終結符,我們首先需要抽象出一個公共父類,即抽象表示式類。
1.所以第一步建立抽象表示式類:
// 抽象表示式類
public abstract class AbstractExpression {
// 提供統一的解釋介面
public abstract int interpret();
}
2.各具體的終結或非終結符類:
數字直譯器類
// 終結符類
public class ValueExpression extends AbstractExpression {
private int value;
public ValueExpression(int value) {
this.value = value;
}
@Override
public int interpret() {
return value;
}
}
加法非終結符類
// 非終結符類
public class AddExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public AddExpression(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() + right.interpret();
}
}
減法非終結符類
// 非終結符類
public class SubtractionExpression extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public SubtractionExpression(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() - right.interpret();
}
}
3.計算器類:
// 計算器類
public class Calculator {
private AbstractExpression expression;
public void parse(String expression) {
String[] expressionArray = expression.split("");
Stack<AbstractExpression> stack = new Stack<>();
for (int i = 0; i < expressionArray.length; i++) {
if ("+".equals(expressionArray[i])) {
AbstractExpression left = stack.pop();
AbstractExpression right = new ValueExpression(Integer.parseInt(expressionArray[++i]));
stack.push(new AddExpression(left,right));
} else if ("-".equals(expressionArray[i])) {
AbstractExpression left = stack.pop();
AbstractExpression right = new ValueExpression(Integer.parseInt(expressionArray[++i]));
stack.push(new SubtractionExpression(left,right));
} else {
stack.push(new ValueExpression(Integer.parseInt(expressionArray[i])));
}
}
this.expression = stack.pop();
}
public int calculate() {
return expression.interpret();
}
}
4.客戶端使用:
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
String expression = "1+2+3+4-5";
calculator.parse(expression);
System.out.println(expression + "=" + calculator.calculate());
}
}
5.使用結果
1+2+3+4-5=5
可以看到結果和上面是一樣的。這裡只是通過案例簡單的運用了一下直譯器模式,但不影響理解設計模式的魅力。
模式應用
雖然直譯器模式的使用頻率不是特別高,但是它在正則表示式、XML文件解釋等領域還是得到了廣泛使用。下面介紹的是其在Spring EL表示式中的典型應用。
1.首先引入這裡需要的 Spring 包:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>design-pattern</artifactId>
<groupId>com.phoegel</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>interpreter</artifactId>
<properties>
<spring.version>5.1.15.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</project>
2.簡單的使用例子:
public class Main {
public static void main(String[] args) {
SpelExpressionParser parser = new SpelExpressionParser();
String expressionStr = "1+2+3+4-5";
Expression expression = parser.parseExpression(expressionStr);
System.out.println(expressionStr + "=" + expression.getValue());
}
}
3.使用結果:
1+2+3+4-5=5
可以看到這裡主要使用了類SpelExpressionParser.parseExpression()
方法返回了Expression
例項,看到這個類名就感覺和直譯器模式很有關係,因此由此深入原始碼會發現Expression
是一個介面,它的具體實現類有CompositeStringExpression
、LiteralExpression
和SpelExpression
等。而這裡使用的SpelExpressionParser
類來獲取Expression
,因此很明顯它將返回SpelExpression
,通過追蹤原始碼也可以證明這一點。
下面是SpelExpressionParser
類中獲取Expression
的關鍵程式碼,它其實最終通過InternalSpelExpressionParser
類中doParseExpression()
方法返回的:
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
throws ParseException {
try {
this.expressionString = expressionString;
// 根據傳入的字串生成 Tokenizer 類例項
Tokenizer tokenizer = new Tokenizer(expressionString);
// 分析詞法生成 List<Token> 集合,在這裡類似於將字串"1+2+3+4-5"分成了一個一個的次,類似於:1、+、2、+、3......
this.tokenStream = tokenizer.process();
this.tokenStreamLength = this.tokenStream.size();
this.tokenStreamPointer = 0;
this.constructedNodes.clear();
// 生成抽象語法樹 ast
SpelNodeImpl ast = eatExpression();
Assert.state(ast != null, "No node");
Token t = peekToken();
if (t != null) {
throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken()));
}
Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected");
// 最後將上面的資料封裝入 SpelExpression 類例項並返回
return new SpelExpression(expressionString, ast, this.configuration);
}
catch (InternalParseException ex) {
throw ex.getCause();
}
}
上面程式碼中最重要的是呼叫了tokenizer.process()
生成詞法集合,以及eatExpression()
方法生成了 ast 抽象語法樹。可以看到 ast 的型別為SpelNodeImpl
,它的子類主要有 Literal,Operator,Indexer等,其中 Literal 是各種型別的值的父類,Operator 則是各種操作的父類。通過執行時的檢視,能夠看到這裡的屬性如下圖示:
根據上面的圖可以詳細的看出整個 ast 抽象語法數的結構及其每個節點的組成。最後就是通過expression.getValue()
方法獲取結果了,程式碼如下:
public Object getValue() throws EvaluationException {
CompiledExpression compiledAst = this.compiledAst;
// 這裡的 compiledAst == null ,所以不會進入判斷
if (compiledAst != null) {
try {
EvaluationContext context = getEvaluationContext();
return compiledAst.getValue(context.getRootObject().getValue(), context);
}
catch (Throwable ex) {
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.compiledAst = null;
this.interpretedCount.set(0);
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION);
}
}
}
ExpressionState expressionState = new ExpressionState(getEvaluationContext(), this.configuration);
// 直接呼叫 this.ast.getValue() 獲取值
Object result = this.ast.getValue(expressionState);
checkCompile(expressionState);
return result;
}
可以看到獲取值的時候是呼叫 ast 中的getValue()
方法的,而這裡的 ast 語法樹的節點型別從上面的 ast 抽象語法樹結構圖可以看出來是OpMinus
類例項,因此會呼叫OpMinus
類中的getValue()
方法,這個方法的核心就是計算減法兩邊表示式的值相減返回結果,由於getValue()
較長,這裡只貼出關鍵程式碼:
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
SpelNodeImpl leftOp = getLeftOperand();
// 判斷型別程式碼省略...
Object left = leftOp.getValueInternal(state).getValue();
Object right = getRightOperand().getValueInternal(state).getValue();
if (left instanceof Number && right instanceof Number) {
Number leftNumber = (Number) left;
Number rightNumber = (Number) right;
if (leftNumber instanceof BigDecimal || rightNumber instanceof BigDecimal) {
}
// 其他判斷型別程式碼省略...
else if (CodeFlow.isIntegerForNumericOp(leftNumber) || CodeFlow.isIntegerForNumericOp(rightNumber)) {
// 這裡是數字做運算,所以會進入這裡並返回相減結果
this.exitTypeDescriptor = "I";
return new TypedValue(leftNumber.intValue() - rightNumber.intValue());
}
else {
// Unknown Number subtypes -> best guess is double subtraction
return new TypedValue(leftNumber.doubleValue() - rightNumber.doubleValue());
}
}
// ...
return state.operate(Operation.SUBTRACT, left, right);
}
整個 SpringEl 表示式中相關聯的類非常多,這裡只是根據例子進行了原始碼追蹤,來更好的理解直譯器模式。
總結
主要優點
-
易於改變和擴充套件文法。由於在直譯器模式中使用類來表示語言的文法規則,因此可以通過繼承等機制來改變或擴充套件文法。
-
每一條文法規則都可以表示為一個類,因此可以方便地實現一個簡單的語言。
-
實現文法較為容易。在抽象語法樹中每一個表示式節點類的實現方式都是相似的,這些類的程式碼編寫都不會特別複雜,還可以通過一些工具自動生成節點類程式碼。
-
增加新的解釋表示式較為方便。如果使用者需要增加新的解釋表示式只需要對應增加一個新的終結符表示式或非終結符表示式類,原有表示式類程式碼無須修改,符合“開閉原則”。
主要缺點
- 對於複雜文法難以維護。在直譯器模式中,每一條規則至少需要定義一個類,因此如果一個語言包含太多文法規則,類的個數將會急劇增加,導致系統難以管理和維護,此時可以考慮使用語法分析程式等方式來取代直譯器模式。
- 執行效率較低。由於在直譯器模式中使用了大量的迴圈和遞迴呼叫,因此在解釋較為複雜的句子時其速度很慢,而且程式碼的除錯過程也比較麻煩。
適用場景
- 可以將一個需要解釋執行的語言中的句子表示為一個抽象語法樹。
- 一些重複出現的問題可以用一種簡單的語言來進行表達。
- 一個語言的文法較為簡單。
- 執行效率不是關鍵問題。【注:高效的直譯器通常不是通過直接解釋抽象語法樹來實現的,而是需要將它們轉換成其他形式,使用直譯器模式的執行效率並不高。】
參考資料
- 大話設計模式
- 設計模式Java版本-劉偉
- 設計模式 | 直譯器模式及典型應用
本篇文章github程式碼地址:https://github.com/Phoegel/design-pattern/tree/main/interpreter
轉載請說明出處,本篇部落格地址:https://www.cnblogs.com/phoegel/p/14140676.html