Interpreter(直譯器)模式
與Command模式一樣,Interpreter模式也會產生一個可執行的物件.差別在於Interpreter模式會建立一個類層次,其中每個類會實現或者解釋一個公共操作,這個操作與類的名稱相匹配.在這方面,Interpreter模式類似於State模式和Strategy模式.在Interpreter模式、State模式以及Strategy模式中,一個公共操作遍佈類集合,每個類都以不同的方式實現這個操作。
Interpreter模式也類似於Composite模式。Composite模式通常會為單個物件和群組物件定義一個公共介面。不過,Composite模式並不要求支援以不同方式組織的結構,儘管該模式可以支援這些結構。例如,介紹Composite模式時曾描述過ProcessComponent類層次結構,該結構允許生產流程順序進行,也允許生產流程交替進行。Interpreter模式通常都會涉及不同型別的組合結構(Interpreter模式通常處於Composite模式結構之上)。一個類組成其他元件的方式定義瞭解釋器類實現或解釋一個操作的方式。
Interpreter模式的主要意圖是可以按照自己定義的組合規則集合來組合可執行物件。
1.Interpreter模式範例:
Oozinoz公司的機器人可以沿著生產線移動原材料。Oozinoz公司開發人員提供一個直譯器,用於控制這些機器人;另外,該直譯器對生產線上的機器具有一定的控制能力。你也許認為直譯器是程式語言,實際上直譯器允許對指令進行組合。Oozinoz機器人直譯器實際上就是一個類層次結構,它封裝了控制機器人的命令。該類層次結構的頂部是一個抽象的Command類。execute()方法遍佈該類層次結構中。圖1顯示了Robot類以及機器人直譯器支援的兩個命令。
直譯器類層次結構提供一種在執行時對工廠機器人進行程式設計的能力
第一眼看該圖時,發現類層次的最高層是Command類,你也許會建議使用Command模式。但是Command模式的目的是在物件中封裝方法。圖1的Command類層次結構不是這樣做的,這個類層次結構要求Command子類重新解釋execute()操作的含義。這就是Interpreter模式的目的:允許你組合複雜的物件。
典型的直譯器類層次包含至少兩個子類,我們將很快擴充套件Command類層次.圖1的兩個類足夠實現本書的範例要求,程式碼如下所示:
package app.interpreter; import com.oozinoz.machine.*: import com.oozinoz.robotInterpreter.*; public class ShowInterpreter { public static void main(String[] args){ MachineComposite dublin = OozinozFactory.dublin(); ShellAssembler assembler = (ShellAssembler)dublin.find("ShellAssembler:3302"); StarPress press = (StarPress)dublin.find("StarPress:3404"); Fuser user = (Fuser)dublin.find("Fuser:"); assembler.load(new Bin(11011)); press.load(new Bin(11015)); CarryCommand carry1 = new CarryCommand(assembler,fuser); CarryCommand carry2 = new CarryCommand(press,fuser); CommandSequence seq = new CommandSequence(); seq.add(carry1); seq.add(carry2); seq.execute(); } }
上述演示程式碼讓工廠機器人把兩箱原材料從操作機器轉移到一個傾銷的緩衝池中,這需要使用OozinozFactory類的dublin()方法返回的機器組合物件.這個資料模型表示位於愛爾蘭都柏林的新Oozinoz工廠.上述程式碼定位這個工廠中的三臺機器,把原材料箱放在其中兩臺機器上面,組合Command類層次結構提供的命令.程式的最後一條語句CommandSequence物件的execute()方法,導致機器人按照seq命令中的指令開始操作.
CommandSequence物件解釋execute()操作,把整個方法呼叫轉發給每個子命令:
package com.oozinoz.robotInterpreter;
import java.util.ArrayList;
import java.util.List;
public class CommandSequence extends Command {
protected List commands = new ArrayList();
public void add(Command c){
commands.add(c);
}
public void execute(){
for(int i=0;i<commands.size();i++){
Command c = (Command)commands.get(i); c.execute();
}
}
}
CarryCommand類完成execute()操作的過程如下 :與工廠機器人進行互動,把材料箱從一個機器移到其他機器.
package com.oozinoz.robotInterpreter;
import com.oozinoz.machine.Machine;
public class CarryCommand extends Command {
protected Machine fromMachine;
protected Machine toMachine;
public CarryCommand( Machine fromMachine,Machine toMachine){
this.fromMachine = fromMachine;
this.toMachine = toMachine;
}
public void execute(){
Robot.singleton.carry(fromMachine,toMachine);
}
}
CarryCOmmand類專用於機器人控制工廠生產線的領域環境下.我們很容易想像其他領域相關的類,諸如控制機器的StartUpCommand和ShutdownCommand類.建立操作多個特定機器的Forcommand類也是有用的.圖2給出對Command類層次的擴充套件.
Interpreter模式允許多個子類重新解釋公共操作的含義
ForCommand類的部分設計思路是非常顯而易見的.推測起來,該類的構造器接收機器集合和命令物件,以便作為for迴圈體處理.這種設計更難的部分是如何把方法體和迴圈連線起來.Java5對for語句進行了擴充套件,建立了一個新變數,每次迴圈體執行一次,該變數就接收一個新值.我們將模擬這種處理方式.請參考如下語句:
for(Command c:commands) c.execute();
Java把for語句宣告中的c識別符號與迴圈體中的c變數連結起來.為建立模擬這種處理方式的直譯器類,我們需要處理和計算變數的方式.圖3的Term類層次結構說明了處理方式.
提供變數對機器進行處理的Term類層次結構
Term類層次結構類似於Command類層次結構的地方在於類層次中到處存在的公共操作(eval()).你也許認為這個類層次本身是Interpreter模式的範例,儘管它並沒有提供通常與Interpreter模式一起使用的組合類,諸如CommandSequence.
Term類層次結構允許我們把每個機器命名為常量,可以給這些常量或者其他變數分配變數.也允許我們讓域相關的直譯器類更加靈活.比如,StartUpCommand程式碼可以與Term物件一起使用,而不是隻能應用於特定機器.
package com.oozinoz.robotInterpreter2;
import com.oozinoz.machine.Machine;
public class StartUpCommand extends Command {
protected Term term;
public StartUpCommand(Term term){
this.term = term;
}
public void execute(){
Machine m = term.eval();
m.startup();
}
}
同樣,為了增強CarrryCommand類的靈活性,我們可以修改該類,以便於使用Term物件,修改程式碼如下:
package com.oozinoz.robotInterpreter2;
public class CarryCommand extends Command {
protected Term from;
protected Term to;
public CarryCommand(Term fromTerm,Term toTerm){
from = fromTerm;
to = toTerm;
}
public void execute(){
Robot.singleton.carry(from.eval(),to.eval());
}
}
一旦Command類層次可以操作Term物件,就可以修改ForCommand類,使之設定變數的值,並在迴圈中執行方法體命令.
package com.oozinoz.robotInterpreter2;
import java.util.List;
import com.oozinoz.machine.Machine;
import com.oozinoz.machine.MachineComponent;
import com.oozinoz.machine.MachineComposite;
public class ForCommand extends Command {
protected MachineComponent root;
protected Variable variable;
protected Command body;
public ForCommand(MachineComponent mc,Variable v,Command body){
this.root = mc;
this.variable = variable;
this.body = body;
}
public void execute(){
execute(root);
}
public void execute(MachineComponent mc){
if(mc instanceof Machine){
Machine m = (Machine)mc;
variable.assign(new Constant(m));
body.execute(); return;
}
MachineComposite comp = (MachineComposite)mc;
List children = comp.getComponents();
for(int i = 0;i<children.size();i++){
MachineComponent child = (MachineComponent)children.get(i);
execute(child);
}
}
}
ForCommand類中的execute()程式碼使用投射來檢視機器元件樹.28章將介紹一種更好的技術來對組合結構進行迭代處理.對於Interpreter模式,最重要的一點是為樹中每個節點正確解釋execute()請求.
藉助於ForCommand類,我們可以開始為工廠組合命令的"程式"或者"指令碼".比如,下面程式組合一個直譯器物件,可以關閉一個工廠中的所有機器.
package app.interpreter;
import com.oozinoz.machine.*;
import com.oozinoz.robotInterpreter2.*;
class ShowDown {
public static void main(String[] args){
MachineComposite dublin = OozinozFactory.dublin();
Variable v = new Variable("machine");
Command c = new ForCommand(dublin,v,new ShutDownCommand(v));
c.execute();
}
}
當這個程式呼叫execute()方法時,ForCommand物件c會解釋execute()方法,方式為:遍歷所提供的機器元件,並且為每個機器完成如下操作:
(1)設定變數v的值;
(2)呼叫已提供ShutDownCommand物件的execute()操作.
如果我們新增控制處理邏輯請求的類,諸如IfCommand類和WhileCommand類,可以得到一個更加完善的直譯器.這些類需要某種方式來模擬Boolean條件.比如,我們也許需要某種方式來模擬機器變數是否等於特定機器的情況.我們也許會引入新的Term類層次結構,但是如果借用C語言的處理思想會更加簡單:讓null代表假,其餘的值代表真.藉助於這個思路,我們可以擴充套件Term類層次結構.如下圖所示:
Term類層次結構包含模擬Boolean條件的類
Equals類比較兩個條件,並返回null來代表假.更合理的設計思路是如果兩個條件相等,Equals類的eval()方法返回其中一個條件,如下所示:
package com.oozinoz.robotInterpreter2;
import com.oozinoz.machine.Machine;
public class Equals extends Term {
protected Term term1;
protected Term term2;
public Equals(Term term1,Term term2){
this.term1 = term1;
this.term2 = term2;
}
public Machine eval(){
Machine m1 = term1.eval();
Machine m2 = term2.eval();
return m1.equals(m2)?m1:null;
}
}
HasMaterial類把Boolean類中值的概念擴充套件到特定領域的例子,諸如如下程式碼:
package com.oozinoz.robotInterpreter2;
import com.oozinoz.machine.Machine;
public class HasMaterial extends Term {
protected Term term;
public HasMaterial(Term term){
this.term = term;
}
public Machine eval(){
Machine m = term.eval();
return m.hasMaterial()?m:null;
}
}
現在我們已經把Boolean條件的概念新增到直譯器包,接下來可以新增流程控制類,如圖5所示.
通過往直譯器類層次結構新增流程控制邏輯,直譯器功能更加豐富
當我們需要一個不做任何事情的命令時,NullCommand類很有用處,諸如當if命令的else分支是空時:
package com.oozinoz.robotInterpreter2;
public class NullCommand extends Command {
public void execute(){ }
}
package com.oozinoz.robotInterpreter2;
public class IfCommand extends Command {
protected Term term;
protected Command body;
protected Command elseBody;
public IfCommand( Term term,Command body,Command elseBody){
this.term = term;
this.body = body;
this.elseBody = elseBody; }
public void execute(){
if(term.eval() != null) body.execute();
else elseBody.execute();
}
}
突破題:請完成IfCommand類中execute()方法的程式碼(上面已經寫出).
突破題:請完成WhileCommand類的程式碼.
package com.oozinoz.robotInterpreter2;
public class WhileCommand extends Command {
protected Term term; protected Command body;
public WhileCommand(Term term,Command body){
this.term = term;
this.body = body;
}
public void execute(){
while(term.eval()!= null) body.execute();
}
}
你也許把WhileCommand類與能夠解除安裝火藥球填壓機的直譯器一起使用,程式碼如下:
package app.interpreter;
import com.oozinoz.machine.*;
import com.oozinoz.robotInterpter2.*;
public class ShowWhile {
public static void main(String[] args){
MachineComposite dublin = OozinozFactory.dubline();
Term starPress = new Constant( (Machine) dublin.find("StarPress:1401"));
Term fuser = new Constant( (Machine) dublin.find("Fuser:1101"));
starPress.eval().load(new Bin(77));
starPress.eval().load(new Bin(88));
whileCommand command = new WhileCommand(
new HasMaterial(starPress),new CarryCommand(starPress,fuser));
command.execute();
}
}
Command物件是個直譯器,可以解釋execute()方法,從火藥球填壓機1401解除安裝所有火藥箱.
突破題:解釋Command模式和Interpreter模式之間的區別.答:Interpreter模式的意圖在於使開發人員可以組合可執行物件,這些物件來自於對某公共操作提供各種解釋的類層次結構.命令模式的意圖僅僅是在一個物件中請求.
直譯器物件能否作為命令使用?當然可以.具體應該使用哪種設計模式,這取決於你的意圖.比如,你是期望建立組合可執行物件的工具集?還是期望將請求封裝在物件中?總之,不同的需求要求使用不同的設計模式.
如想增加其他應用領域相關任務的控制,我們可以往直譯器類層次結構新增更多功能.也可以擴充套件Term類層次結構.比如讓Term子類發現靠近其他機器的解除安裝緩衝池.
Command和Term類層次結構的使用者可以隨意組合豐富而複雜的執行"程式".比如,建立下面一個物件並不難,當這個物件執行時,將解除安裝除工廠中解除安裝緩衝池之外的來自於所有機器的原材料.我們可以使用虛擬碼來描述這個程式,如下所示:
for(m in factory)
if (not (m is unloadBuffer))
ub = findUnload for m
while( m hasMaterial) carry(m,ub)
如果要編寫Java程式碼來完成這些任務,相對於虛擬碼而言,Java程式碼會更加龐大和複雜.所以為什麼不使用虛擬碼,通過建立獲取領域語言的解析器來管理工廠中的原材料,並建立對應的直譯器物件呢?
2.直譯器、語言和解析器:
Interpreter模式意在強調直譯器的工作方式,但是它並沒有指定如何在執行時組合或者例項化它們。在本章,我們通過編寫Java程式碼來“手工”生成新的直譯器。但是建立一個新的直譯器最常見的方法就是使用解析器。解析器物件可以按照預定規則識別文字和分解文字結構,以便於進一步處理。比如,你可以編寫一個解析器,用它建立一個對應到早期虛擬碼形式文字程式的機器命令直譯器物件。 用Java語言編寫的解析器工具很少,討論這個主題的書籍也非常少。可以使用關鍵字“Java 解析器工具"來搜尋網路資源。大多數解析器都包含一個解析器生成器。為了使用這個生成器,可以使用專門的語法來描述這個模式,工具會根據描述產生解析器。所產生的解析器會識別出你的語言例項。有時,不必使用解析器生成工具,可以通過應用Interpreter模式來編寫通用目的的解析器。Builder Parser with Java[Metsker,2001]使用Java語言解釋了這種技術。
3. 小結:
Interpreter模式使我們可以根據建立的類層次結構來組合可執行物件。該類層次結構中的每個類分別實現一個公共操作,諸如execute()。儘管前面沒有討論過,但這個方法需要一個“上下文”物件,用來儲存重要的狀態資訊。
每個類的名稱通常意味著這個類實現這個公共操作的方式。每個類或者定義組合命令的方式,或者導致某種動作的終端命令。
直譯器通常伴隨著引入變數的設計,以及Boolean或算術表示式。直譯器也經常使用解析器來簡化新的直譯器物件的建立過程。