對於一個具有層次節點關係的問題來說,如果您要剖析每一個節點,您可以使用Interpreter模式,直譯器模式有些類似演算法中的個別擊破方式,對每一個父節點我們剖析出其子節點組合,然而交給子節點剖析物件繼續剖析,直到剖析至終端節點為止。
舉個例子來說明好了,先說明的是,這個例子是改寫自 Design Patterns於Java語言之實習應用 第23章的範例,我將之更簡化了,以讓大家將焦點能集中在如何使用Interpreter模式,以及如何實用。
假設您要實作一個Interpreter,這個Interpreter可以直譯您文字檔中的程式,並依您自訂的程式文法來執行程式,幾個簡單的程式如下:
PROGRAM
PRINT dog SPACE
PRINT is SPACE
PRINT an SPACE
PRINT animai
END
您的這式程個會印出"dog is an animal"的文字,再來一個例子是:
PROGRAM
REPEAT 2
LINEBREAK
PRINT dog
BREAK
END
END
這個程式要印出:
------------------------------
dog
------------------------------
dog |
您也可以任意的組合程式,例如:
PROGRAM
PRINT begin
BREAK
REPEAT 3
REPEAT 2
PRINT dog SPACE
PRINT is SPACE
PRINT a SPACE
PRINT animal
BREAK
END
END
END
這個程式中的幾個關鍵字是PROGRAM、PRINT、SPACE、BREAK、LINEBREAK、REPEAT、END,
PROGRAM是表示程式開始,以END作結,PRINT可以印出一個無空白的字串,SPACE印出一個空白,BREAK是換行,而LINEBREAK是
畫一個直線並換行,REPEAT是迴圈指令,可以指定迴圈次數,以END作結。
觀察程式,可以制定出以下的文法,如下:
<program> ::= PROGRAM <command list>
<command list> ::= <command>* END
<command> ::= <repeat command> | <primitive command>
<repeat command> ::= REPEAT <number> <command list>
<primitive command> ::= PRINT <string>
| BREAK | SPACE | LINEBREAK
程式文法制定需要對程式進行語句分析與定義,在這邊並不討論這個課題,在程式中,command節點由primitive或repeat兩個節點任意組
合,一個command list節點則是零個以上的command節點組合而成,其中repeat還可以組合command
list節點,這是組合模式的應用,可以在程式中組合巢狀迴圈。
在直譯程式時,以讀到PROGRAM作為開始節點,接下來我們剖析程式為command list 節點,並將它們丟給專門剖析command
list的物件繼續剖析,這個物件將之分析,看是不是有repeat command或primitive
command節點,如果有就再往下交由專屬物件進行剖析,如此層層剝開,並由專屬物件負責剖析工作。
Interpreter模式的基本觀念就如上所示,先來看看如何以程式實現剖析的過程,下面這個程式會剖析您的程式,並將程式加上對應的括號來將同一個區塊組合起來,以表示它完成剖析之後的結果:
public interface INode { public void parse(Context context); }
// <program> ::= PROGRAM <command list> public class ProgramNode implements INode { private INode commandListNode; public void parse(Context context) { context.skipToken("PROGRAM"); commandListNode = new CommandListNode(); commandListNode.parse(context); }
public String toString() { return "[PROGRAM " + commandListNode + "]"; } }
import java.util.Vector;
// <command list> ::= <command>* END public class CommandListNode implements INode { private Vector list = new Vector();
public void parse(Context context) { while (true) { if (context.currentToken() == null) { System.err.println("Missing 'END'"); break; } else if ( context.currentToken().equals("END")) { context.skipToken("END"); break; } else { INode commandNode = new CommandNode(); commandNode.parse(context); list.add(commandNode); } } }
public String toString() { return "" + list; } }
// <command> ::= <repeat command> | <primitive command> public class CommandNode implements INode { private INode node;
public void parse(Context context) { if (context.currentToken().equals("REPEAT")) { node = new RepeatCommandNode(); node.parse(context); } else { node = new PrimitiveCommandNode(); node.parse(context); } }
public String toString() { return node.toString(); } }
public class RepeatCommandNode implements INode { private int number; private INode commandListNode;
public void parse(Context context) { context.skipToken("REPEAT"); number = context.currentNumber(); context.nextToken(); commandListNode = new CommandListNode(); commandListNode.parse(context); }
public String toString() { return "[REPEAT " + number + " " + commandListNode + "]"; } }
- PrimitiveCommandNode.java
// <primitive command> ::= PRINT <string> // | SPACE | BREAK | LINEBREAK public class PrimitiveCommandNode implements INode { private String name; private String text;
public void parse(Context context) { name = context.currentToken(); context.skipToken(name); if (!name.equals("PRINT") && !name.equals("BREAK") && !name.equals("LINEBREAK") && !name.equals("SPACE")) { System.err.println("Undefined Command"); }
if (name.equals("PRINT")) { text = context.currentToken(); name += text; context.nextToken(); } }
public String toString() { return name; } }
import java.util.*;
public class Context { private StringTokenizer tokenizer; private String currentToken;
public Context(String text) { tokenizer = new StringTokenizer(text); nextToken(); }
public String nextToken() { if (tokenizer.hasMoreTokens()) { currentToken = tokenizer.nextToken(); } else { currentToken = null; } return currentToken; }
public String currentToken() { return currentToken; }
public void skipToken(String token) { if (!token.equals(currentToken)) { System.err.println("Warning: " + token + " is expected, but " + currentToken + " is found."); } nextToken(); }
public int currentNumber() { int number = 0; try { number = Integer.parseInt(currentToken); } catch (NumberFormatException e) { System.err.println("Warning: " + e); } return number; } }
import java.util.*; import java.io.*;
public class Main { public static void main(String[] args) { try { BufferedReader reader = new BufferedReader(new FileReader(args[0])); String text; while ((text = reader.readLine()) != null) { System.out.println("text = \"" + text + "\""); INode node = new ProgramNode(); node.parse(new Context(text)); System.out.println("node = " + node); } } catch (ArrayIndexOutOfBoundsException e) { System.err.println( "Usage: java Main yourprogram.txt"); } catch (Exception e) { e.printStackTrace(); } } }
假設您的程式是這樣寫的:
PROGRAM PRINT xxx END PROGRAM REPEAT 4 PRINT xxx END END PROGRAM REPEAT 4 PRINT xxx PRINT "yyy" END END
則執行Intrepreter程式之後會是:
$ java Main program.txt
text = "PROGRAM PRINT xxx END"
node = [PROGRAM [PRINTxxx]]
text = "PROGRAM REPEAT 4 PRINT xxx END END"
node = [PROGRAM [[REPEAT 4 [PRINTxxx]]]]
text = "PROGRAM REPEAT 4 PRINT xxx PRINT "yyy" END END"
node = [PROGRAM [[REPEAT 4 [PRINTxxx, PRINT"yyy"]]]] |
這個範例程式基本上已經顯示了直譯器模式的工作原理,如何讓程式直譯之後能夠工作,這待會再示範,先來看一下Intrepreter模式的 UML 類別結構圖:
TerminalExpression就像我們的primitive
command,再剖析下去已經沒有子節點了,而NonterminalExpression就像是repeat
command,注意到其中也使用了組合模式,就如之前所說的,組合模式讓可以遞迴的組合句子為更複雜的語句。
您已經會剖析句子了,接下來要如何讓這個直譯器真正工作,雖然程式中使用toString()來表示每一個節點的剖析結果,但事實上,這個程式也已經說明
了如何讓剖析的結果真正運作了,既然已經記錄好剖析之後的語句順序了,只要由上而下追蹤剖析結果,就一定可以執行到 primitive
command,且順序符合自訂的程式原始碼的需求,這只要將toString()改為execute(),並作一些轉發與重複執行的修改就可以了,直接
來看程式會比較容易理解:
public interface INode { public void parse(Context context); public void execute(); }
// <program> ::= PROGRAM <command list> public class ProgramNode implements INode { private INode commandListNode;
public void parse(Context context) { context.skipToken("PROGRAM"); commandListNode = new CommandListNode(); commandListNode.parse(context); }
public void execute() { commandListNode.execute(); }
public String toString() { return "[PROGRAM " + commandListNode + "]"; } }
import java.util.*;
// <command list> ::= <command>* END public class CommandListNode implements INode { private Vector list = new Vector(); private INode commandNode;
public void parse(Context context) { while (true) { if (context.currentToken() == null) { System.err.println("Missing 'END'"); break; } else if(context.currentToken().equals("END")) { context.skipToken("END"); break; } else { commandNode = new CommandNode(); commandNode.parse(context); list.add(commandNode); } } }
public void execute() { Iterator it = list.iterator(); while (it.hasNext()) { ((CommandNode)it.next()).execute(); } }
public String toString() { return "" + list; } }
// <command> ::= <repeat command> | <primitive command> public class CommandNode implements INode { private INode node;
public void parse(Context context) { if (context.currentToken().equals("REPEAT")) { node = new RepeatCommandNode(); node.parse(context); } else { node = new PrimitiveCommandNode(); node.parse(context); } }
public void execute() { node.execute(); }
public String toString() { return node.toString(); } }
- PrimitiveCommandNode.java
// <primitive command> ::= PRINT <string> // | SPACE | BREAK | LINEBREAK public class PrimitiveCommandNode implements INode { private String name; private String text;
public void parse(Context context) { name = context.currentToken(); context.skipToken(name); if (!name.equals("PRINT") && !name.equals("BREAK") && !name.equals("LINEBREAK") && !name.equals("SPACE")) { System.err.println("Undefined Command"); }
if (name.equals("PRINT")) { text = context.currentToken(); context.nextToken(); } }
public void execute() { if(name.equals("PRINT")) System.out.print(text); else if(name.equals("SPACE")) System.out.print(" "); else if(name.equals("BREAK")) System.out.println(); else if(name.equals("LINEBREAK")) System.out.println( "\n------------------------------"); }
public String toString() { return name; } }
public class RepeatCommandNode implements INode { private int number; private INode commandListNode;
public void parse(Context context) { context.skipToken("REPEAT"); number = context.currentNumber(); context.nextToken(); commandListNode = new CommandListNode(); commandListNode.parse(context); }
public void execute() { for(int i = 0; i < number; i++) commandListNode.execute(); }
public String toString() { return "[REPEAT " + number + " " + commandListNode + "]"; } }
import java.util.*;
public class Context { private StringTokenizer tokenizer; private String currentToken;
public Context(String text) { tokenizer = new StringTokenizer(text); nextToken(); }
public String nextToken() { if (tokenizer.hasMoreTokens()) { currentToken = tokenizer.nextToken(); } else { currentToken = null; } return currentToken; }
public String currentToken() { return currentToken; }
public void skipToken(String token) { if (!token.equals(currentToken)) { System.err.println("Warning: " + token + " is expected, but " + currentToken + " is found."); } nextToken(); }
public int currentNumber() { int number = 0; try { number = Integer.parseInt(currentToken); } catch (NumberFormatException e) { System.err.println("Warning: " + e); } return number; } }
import java.util.*; import java.io.*;
public class Main { public static void main(String[] args) { try { BufferedReader reader = new BufferedReader( new FileReader(args[0])); String text; while ((text = reader.readLine()) != null) { System.out.println("text = \"" + text + "\""); INode node = new ProgramNode(); node.parse(new Context(text)); node.execute(); } } catch (ArrayIndexOutOfBoundsException e) { System.err.println( "Useage: java Main yourprogram.txt"); } catch (Exception e) { e.printStackTrace(); } } }
假設您的直譯程式稿是這麼撰寫的:
PROGRAM REPEAT 4 LINEBREAK PRINT justin SPACE PRINT momor LINEBREAK END END
則程式執行的結果就是:
$ java Main program.txt
text = "PROGRAM REPEAT 4 LINEBREAK PRINT justin SPACE
PRINT momor LINEBREAK END END"
------------------------------
justin momor
------------------------------
------------------------------
justin momor
------------------------------
------------------------------
justin momor
------------------------------
------------------------------
justin momor
------------------------------ |
Design Patterns於Java語言之實習應用 第23章的範例中,可以讓您依指令稿直譯,畫出任何的圖案,讓範例結合了工廠(Factory)模式、外觀(Facade)模式等等,在這邊建議您看看那個範例,看看不同的設計模式之間如何組合且相互合作。 |