設計模式(Java)—Interpreter模式
在Interpreter模式中,程式要解決的問題會被用非常簡單的“迷你語言”表述出來,即用“迷你語言”編寫的“迷你程式”把具體問題表述出來。迷你程式是無法單獨工作的,我們還需要用Java語言編寫一個負責“翻譯”的程式。翻譯程式會理解迷你語言,並解釋和執行迷你程式。這段翻譯程式也稱為直譯器。這樣,當需要解決的問題發生變化時,不需要修改Java語言程式,只需要修改迷你語言程式即可應對。
使用Java語言程式設計時,需要修改程式碼,但是,在使用Interpreter模式後,我們就無需修改Java程式,只需修改迷你語言編寫的迷你程式即可。
迷你語言
此次迷你語言的用途是控制無線玩具車。雖說是玩具車,其實能做的事情不過以下3種。
- 前進1米(go)
- 右轉(right)
- 左轉(left)
- 重複(repeat)
以上就是可以向玩具車傳送的命令,go是前進1米後停止的命令;right是原地向右轉的命令,left是原地向左轉的命令。在實際操作時,是不能完全沒有偏差地原地轉彎的,為了使問題簡單化,我們這裡並不會改變玩具車的位置,而是像將其放在旋轉桌子上一樣,讓他轉個方向。
以上命令組合起來就是可以控制無線玩具車的迷你語言了。
控制無線玩具車的迷你語言
迷你語言程式示例
下面我們來看一段用迷你語言編寫的迷你程式。下面這條語句可以控制無線玩具車前進(之後停止)。
program go end
為了便於大家看出語句的開頭和結尾,我們在語句前後分別加上了program和end關鍵字。這個迷你程式的執行結果如下。
program go end的執行結果
接下來是一段讓無限玩具車先前進一米,接著讓它右轉兩次再返回的程式。
program go right right go end
再接下來的這段程式是讓無線玩具車按照正方形路徑行進,其執行結果如下所示。
program go right go right go right go right end…(A)
program go right go right go right go right end的執行結果
(A)程式的最後(即end之前)之所以加上了right,是因為當無線玩具車回到起點後,我們希望它的方向與出發時相同,在(A)程式中,重複出現了4次go right。這樣,我們可以使用repeat…end語句來實現下面的(B)程式(為了能夠編寫出這段程式,我們需要定義迷你語言的語法),其執行結果如圖所示。
program repeat 4 go right end end …(B)
在(B)程式的最後出現了兩個end,其中第一個(左邊)end表示repeat的結束,第二個(右邊)end表示program的結束,也就是說,程式的結構如下:
再如:
program repeat 4 repeat 3 go right go left end right end end
現在玩具車會按照如下圖所示的鋸齒形狀路線前進。
這段程式可分解如下形式:
內側的迴圈語句是go right go left,它是一條讓無線玩具車“前進後右轉,前進後左轉”的命令。該命令會重複3次。這樣,玩具車就會向右沿著鋸齒形路線行進,接著,退至外側迴圈看,玩具車會連續4次“沿著鋸齒形路線進行一次後,右轉一次”。這樣,最終行進路線就變成了一個鋸齒樣的菱形。
迷你語言的語法
上圖展示了迷你語言的語法。這裡使用的描述方法是BNF的一個變種,BNF經常被用於描述演算法。
<program>::=program <command list>
首先我們定義了程式<program>,即“所謂<program>,是指program關鍵字後面跟著的命令列表<command list>”,“::=”的左邊表示定義的名字,右邊表示定義的內容。
<command list> ::=<command>* end
接著,我們定義了命令列表<command list>,即“所謂<command list>,是指重複0次以上<command>後,接著一個end關鍵字”,“*”表示前面的內容迴圈0次以上。
<command> ::=<repeat command> | <primitive command>
現在,我們來定義<command>,即“所謂<command>,是指<repeat command>或<primitive command>”。
<repeat command>::= repeat <number><command list>
接下來,我們定義迴圈命令,即“所謂<repeat command>,是指repeat關鍵字後面跟著迴圈次數<number>”和要迴圈的命令列表<command list>。其中<command list>在之前已經定義過了,而在定義命令列表的時候使用了<command>,在定義<command>的時候又使用了<command>,而在定義<repeat command>的時候又使用了<command list>,像這樣,在定義某個東西時,他自身又出現在了定義的內容中,我們稱這種定義為遞迴定義。
<primitive command>::= go | right | left
這是基本命令<primitive command>的定義,即“所謂<primitive command>是指go或者right或者left”。
終結符表示式與非終結符表示式
前面講到的想<primitive command>這樣的不會進一步展開的表示式被成為“終結符表示式”。與之相對的是,像<command>和<repeat command>這樣的需要被進一步展開的表示式被成為“非終結符表示式”。
示例程式
示例程式實現了一個迷你程式的語法解析器。
在之前學習迷你程式的相關內容時,我們分別學習了迷你程式的各個語法部分。像這樣將迷你程式當作普通字元分解,然後看看各個部分分別是什麼結構的過程,就是語法解析。
例如有迷你程式
program repeat 4 go right end end
將這段迷你程式推導成為下圖那樣的結構(語法樹)的處理,就是語法解析。
示例程式的類圖
Node類
該類是語法樹中各個部分中最頂層的節點。
package Interpreter;
//直譯器的語法樹總結點類,該類為抽象類,其中宣告另外抽象方法parse,引數傳遞為語法分析的文字上下文物件
public abstract class Node {
//當標記解析出錯時丟擲異常,列印異常資訊
public abstract void parse(Context context) throws ParseException;
}
ProgramNode類
package Interpreter;
//Program節點類, <program> ::= program <command list>
public class ProgramNode extends Node {
//定義命令列表變數
private Node commandListNode;
public void parse(Context context) throws ParseException {
//跳過program標記
context.skipToken("program");
//建立commandList例項
commandListNode = new CommandeListNode();
//將上文內容傳遞給commandList例項解析
commandListNode.parse(context);
};
//重寫toString方法
public String toString(){
return "[program"+commandListNode+"]";
}
}
CommandList類
package Interpreter;
import java.util.ArrayList;
//CommandeListNode類 <command list> ::= <command>* end
//重複0次以上command 然後以end結束
public class CommandeListNode extends Node {
//儲存command例項
private ArrayList list = new ArrayList<>();
public void parse(Context context) throws ParseException {
while(true){
//當前標記為空,丟擲異常,缺少end關鍵字
if(context.currentToken()==null){
throw new ParseException("Missing end");
}else if(context.currentToken().equals("end")){
//當前標記為end,則解析完畢,跳過end標記,break出迴圈,結束解析工作
context.skipToken("end");
break;
}else {
//否則,建立command例項,將context交由command例項解析
Node commandNode = new CommandNode();
commandNode.parse(context);
//將command例項新增至列表中
list.add(commandNode);
}
}
}
public String toString(){
return list.toString();
}
}
CommandNode類
package Interpreter;
public class CommandNode extends Node {
//儲存RepeatCommandNode或PrimitiveCommandNode例項
private Node node;
public void parse(Context context) throws ParseException {
//如果當前標記是repeat,則建立RepeatCommandNode例項,交由其解析
if(context.currentToken().equals("repeat")){
node = new RepeatCommandNode();
node.parse(context);
}else {
//否則交由PrimitiveCommandNode例項解析
node = new PrimitiveCommandNode();
node.parse(context);
}
}
public String toString(){
return node.toString();
}
}
RepeatCommandNode類
package Interpreter;
public class RepeatCommandNode extends Node {
private Node commandListNode;
private int number;
public void parse(Context context) throws ParseException {
//跳過repeat標記
context.skipToken("repeat");
//repeat標記後面一定是數字
number = context.currentNumber();
context.nextToken();
//repeat標記後面又可以還是命令列表,所以繼續呼叫CommandeListNode例項解析
commandListNode = new CommandeListNode();
commandListNode.parse(context);
}
public String toString(){
return "[repeat "+number+" "+commandListNode+"]";
}
}
PrimitiveCommandNode類
package Interpreter;
public class PrimitiveCommandNode extends Node {
//儲存當前標記即動作名稱
private String name;
public void parse(Context context) throws ParseException {
//獲取當前標記
name = context.currentToken();
//跳至下一標記
context.nextToken();
//判斷,若果標記不是go、right、left其中一個的話,丟擲異常
if(!name.equals("go")&&!name.equals("right")&&!name.equals("left")){
throw new ParseException(name+ "is undefined");
}
}
public String toString(){
return name;
}
}
Context類
package Interpreter;
import java.util.StringTokenizer;
//被解析的內容
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)throws ParseException{
if(!token.equals(currentToken)){
throw new ParseException("warning:"+token+"is expected,but"+currentToken+"is found.");
}
nextToken();
}
//將字元數字轉換為數字格式的數字
public int currentNumber() throws ParseException{
int number;
try {
number = Integer.parseInt(currentToken);
} catch (NumberFormatException e) {
// TODO: handle exception
throw new ParseException("warning:"+e);
}
return number;
}
}
ParseException類
package Interpreter;
public class ParseException extends Exception {
public ParseException(String msg) {
// TODO Auto-generated constructor stub
super(msg);
}
}
Main類
package Interpreter;
import java.io.BufferedReader;
import java.io.FileReader;
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
//從文字中逐行讀取內容,對每行內容進行解析
BufferedReader bufferedReader = new BufferedReader(new FileReader("program.txt"));
String text;
while((text=bufferedReader.readLine())!=null){
System.out.println("text = \""+text+"\"");
//先建立ProgramNode例項,並對該行進行遞迴解析
Node node = new ProgramNode();
node.parse(new Context(text));
System.out.println("node = "+node);
}
//關閉buffer指標
bufferedReader.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
執行結果:
練習
增加以下功能:
- 示例程式中只進行了語法分析,修改程式,讓示例程式還可以“執行”迷你程式語言。
- 使用GUI顯示基本命令的“執行”結果。
- 使用Facade模式使直譯器更易於使用。
- 編寫一個生成基本命令的類。
- 將直譯器的相關程式碼單獨整理至一個包中。
language包
turtle包
測試類
InterpreterFacade類
package language;
public class InterpreterFacade implements Executor {
private ExecutorFactory factory;
private Context context;
private Node programNode;
public InterpreterFacade(ExecutorFactory factory) {
this.factory = factory;
}
public boolean parse(String text) {
boolean ok = true;
this.context = new Context(text);
this.context.setExecutorFactory(factory);
this.programNode = new ProgramNode();
try {
programNode.parse(context);
System.out.println(programNode.toString());
} catch (ParseException e) {
e.printStackTrace();
ok = false;
}
return ok;
}
public void execute() throws ExecuteException {
try {
programNode.execute();
} catch (ExecuteException e) {
e.printStackTrace();
}
}
}
ExecutorFactory介面
package language;
public interface ExecutorFactory {
public abstract Executor createExecutor(String name);
}
Context類
package language;
import java.util.*;
public class Context implements ExecutorFactory {
private ExecutorFactory factory;
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) throws ParseException {
if (!token.equals(currentToken)) {
throw new ParseException("Warning: " + token + " is expected, but " + currentToken + " is found.");
}
nextToken();
}
public int currentNumber() throws ParseException {
int number = 0;
try {
number = Integer.parseInt(currentToken);
} catch (NumberFormatException e) {
throw new ParseException("Warning: " + e);
}
return number;
}
public void setExecutorFactory(ExecutorFactory factory) {
this.factory = factory;
}
public Executor createExecutor(String name) {
return factory.createExecutor(name);
}
}
Node類
package language;
public abstract class Node implements Executor {
public abstract void parse(Context context) throws ParseException;
}
Executor介面
package language;
public interface Executor {
public abstract void execute() throws ExecuteException;
}
ProgramNodel類
package language;
// <program> ::= program <command list>
public class ProgramNode extends Node {
private Node commandListNode;
public void parse(Context context) throws ParseException {
context.skipToken("program");
commandListNode = new CommandListNode();
commandListNode.parse(context);
}
public void execute() throws ExecuteException {
commandListNode.execute();
}
public String toString() {
return "[program " + commandListNode + "]";
}
}
CommandNode類
package language;
// <command> ::= <repeat command> | <primitive command>
public class CommandNode extends Node {
private Node node;
public void parse(Context context) throws ParseException {
if (context.currentToken().equals("repeat")) {
node = new RepeatCommandNode();
node.parse(context);
} else {
node = new PrimitiveCommandNode();
node.parse(context);
}
}
public void execute() throws ExecuteException {
node.execute();
}
public String toString() {
return node.toString();
}
}
RepeatCommandNode類
package language;
// <repeat command> ::= repeat <number> <command list>
public class RepeatCommandNode extends Node {
private int number;
private Node commandListNode;
public void parse(Context context) throws ParseException {
context.skipToken("repeat");
number = context.currentNumber();
context.nextToken();
commandListNode = new CommandListNode();
commandListNode.parse(context);
}
public void execute() throws ExecuteException {
for (int i = 0; i < number; i++) {
commandListNode.execute();
}
}
public String toString() {
return "[repeat " + number + " " + commandListNode + "]";
}
}
CommandListNode類
package language;
import java.util.*;
// <command list> ::= <command>* end
public class CommandListNode extends Node {
private ArrayList list = new ArrayList();
public void parse(Context context) throws ParseException {
while (true) {
if (context.currentToken() == null) {
throw new ParseException("Missing 'end'");
} else if (context.currentToken().equals("end")) {
context.skipToken("end");
break;
} else {
Node commandNode = new CommandNode();
commandNode.parse(context);
list.add(commandNode);
}
}
}
public void execute() throws ExecuteException {
Iterator it = list.iterator();
while (it.hasNext()) {
((CommandNode)it.next()).execute();
}
}
public String toString() {
return list.toString();
}
}
PrimitiveCommandNode類
package language;
// <primitive command> ::= go | right | left
public class PrimitiveCommandNode extends Node {
private String name;
private Executor executor;
public void parse(Context context) throws ParseException {
name = context.currentToken();
context.skipToken(name);
executor = context.createExecutor(name);
}
public void execute() throws ExecuteException {
if (executor == null) {
throw new ExecuteException(name + ": is not defined");
} else {
executor.execute();
}
}
public String toString() {
return name;
}
}
ExecuteException類
package language;
public class ExecuteException extends Exception {
public ExecuteException(String msg) {
super(msg);
}
}
ParseException類
package language;
public class ParseException extends Exception {
public ParseException(String msg) {
super(msg);
}
}
TurtleCanvas類
package turtle;
import language.Executor;
import language.ExecutorFactory;
import language.ExecuteException;
import java.awt.*;
public class TurtleCanvas extends Canvas implements ExecutorFactory {
final static int UNIT_LENGTH = 30; // 前進時的長度單位
final static int DIRECTION_UP = 0; // 上方
final static int DIRECTION_RIGHT = 3; // 右方
final static int DIRECTION_DOWN = 6; // 下方
final static int DIRECTION_LEFT = 9; // 左方
final static int RELATIVE_DIRECTION_RIGHT = 3; // 右轉
final static int RELATIVE_DIRECTION_LEFT = -3; // 左轉
final static int RADIUS = 3; // 半徑
private int direction = 0;
private Point position;
private Executor executor;
public TurtleCanvas(int width, int height) {
setSize(width, height);
initialize();
}
public void setExecutor(Executor executor) {
this.executor = executor;
}
void setRelativeDirection(int relativeDirection) {
setDirection(direction + relativeDirection);
}
void setDirection(int direction) {
if (direction < 0) {
direction = 12 - (-direction) % 12;
} else {
direction = direction % 12;
}
this.direction = direction % 12;
}
void go(int length) {
int newx = position.x;
int newy = position.y;
switch (direction) {
case DIRECTION_UP:
newy -= length;
break;
case DIRECTION_RIGHT:
newx += length;
break;
case DIRECTION_DOWN:
newy += length;
break;
case DIRECTION_LEFT:
newx -= length;
break;
default:
break;
}
Graphics g = getGraphics();
if (g != null) {
g.drawLine(position.x, position.y, newx, newy);
g.fillOval(newx - RADIUS, newy - RADIUS, RADIUS * 2 + 1, RADIUS * 2 + 1);
}
position.x = newx;
position.y = newy;
}
public Executor createExecutor(String name) {
if (name.equals("go")) {
return new GoExecutor(this);
} else if (name.equals("right")) {
return new DirectionExecutor(this, RELATIVE_DIRECTION_RIGHT);
} else if (name.equals("left")) {
return new DirectionExecutor(this, RELATIVE_DIRECTION_LEFT);
} else {
return null;
}
}
public void initialize() {
Dimension size = getSize();
position = new Point(size.width / 2, size.height / 2);
direction = 0;
setForeground(Color.red);
setBackground(Color.white);
Graphics g = getGraphics();
if (g != null) {
g.clearRect(0, 0, size.width, size.height);
}
}
public void paint(Graphics g) {
initialize();
if (executor != null) {
try {
executor.execute();
} catch (ExecuteException e) {
}
}
}
}
abstract class TurtleExecutor implements Executor {
protected TurtleCanvas canvas;
public TurtleExecutor(TurtleCanvas canvas) {
this.canvas = canvas;
}
public abstract void execute();
}
class GoExecutor extends TurtleExecutor {
public GoExecutor(TurtleCanvas canvas) {
super(canvas);
}
public void execute() {
canvas.go(TurtleCanvas.UNIT_LENGTH);
}
}
class DirectionExecutor extends TurtleExecutor {
private int relativeDirection;
public DirectionExecutor(TurtleCanvas canvas, int relativeDirection) {
super(canvas);
this.relativeDirection = relativeDirection;
}
public void execute() {
canvas.setRelativeDirection(relativeDirection);
}
}
Main類
import language.InterpreterFacade;
import turtle.TurtleCanvas;
import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
public class Main extends Frame implements ActionListener {
private TurtleCanvas canvas = new TurtleCanvas(400, 400);
private InterpreterFacade facade = new InterpreterFacade(canvas);
private TextField programTextField = new TextField("program repeat 3 go right go left end end");
// 建構函式
public Main(String title) {
super(title);
canvas.setExecutor(facade);
setLayout(new BorderLayout());
programTextField.addActionListener(this);
this.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
add(programTextField, BorderLayout.NORTH);
add(canvas, BorderLayout.CENTER);
pack();
parseAndExecute();
show();
}
// 供ActionListener用
public void actionPerformed(ActionEvent e) {
if (e.getSource() == programTextField) {
parseAndExecute();
}
}
private void parseAndExecute() {
String programText = programTextField.getText();
System.out.println("programText = " + programText);
facade.parse(programText);
canvas.repaint();
}
public static void main(String[] args) {
new Main("Interpreter Pattern Sample");
}
}