設計模式之(二十)解釋器模式Interpreter

對於一個具有層次節點關係的問題來說,如果您要剖析每一個節點,您可以使用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模式的基本觀念就如上所示,先來看看如何以程式實現剖析的過程,下面這個程式會剖析您的程式,並將程式加上對應的括號來將同一個區塊組合起來,以表示它完成剖析之後的結果:
  • INode.java
public interface INode { 
    public void parse(Context context); 
} 

  • ProgramNode.java
// <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 + "]"; 
    } 
}  

  • CommandListNode.java
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; 
    } 
}  

  • CommandNode.java
// <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(); 
    } 
}  

  • RepeatCommandNode.java
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; 
    } 
}  

  • Context.java
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; 
    } 
}  

  • Main.java
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.txt
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(),並作一些轉發與重複執行的修改就可以了,直接 來看程式會比較容易理解:
  • INode.java
public interface INode {
    public void parse(Context context);
    public void execute();
} 

  • ProgramNode.java
// <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 + "]";
    }
} 

  • CommandListNode.java
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;
   }
} 

  • CommandNode.java
// <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;
    }
} 

  • RepeatCommandNode.java
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 + "]";
    }
} 

  • Context.java
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;
    }
} 

  • Main.java
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.txt
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)模式等等,在這邊建議您看看那個範例,看看不同的設計模式之間如何組合且相互合作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章