單分派、雙分派及兩種設計模式
什麼是單分派和雙分派
分派(dispatch) 是指按照物件的實際型別為其繫結對應方法體的過程。
例如有X類及其兩個子類X1、X2,它們都實現了例項方法m()——通常子類X1、X2的方法前應該加@Override,所以有3個m()。
對於訊息表示式a.m(b,c),按照一個物件的實際型別繫結對應方法體,稱為單分派。當然,這個“一個物件”比較特殊,每一個訊息表示式a.m(b,c)只有一個訊息接收者,這個“一個物件”就是指訊息接收者,即a.m(b,c)中的a。所以,僅按照訊息接收者的實際型別繫結實際型別提供的方法體,即單分派(singledispatch),就是面向物件中的動態繫結!
假設對於訊息表示式a.m(b,c),如果能夠按照a、b和c的實際型別為其繫結對應方法體,則稱為三分派。簡單起見,研究雙分派(double dispatch)就夠了。
所謂的雙分派,則是希望a.foo(b)能夠 ①按照a的實際型別繫結其override的方法體,而且能夠 ②按照b的實際型別繫結其過載的方法即foo(Y)、foo(Y1)、foo(Y2)中的適當方法體。 【相關概念,可以參考《設計模式.5.11訪問者模式》p223】
遺憾的是,Java不支援雙分派。對於foo(X)、foo(X1)和foo(X2)這些過載的方法,Java在編譯時,就為foo(b)按照b的宣告型別靜態綁定了foo(X)這個的方法體,而不會去判斷b的實際型別是X1還是X2。 Java中可以使用執行時型別識別(Run-Time TypeIdentification、RTTI)技術,即使用關鍵字instanceof判斷實際型別。雖然宣告型別為父類Y,程式中按照實際型別重新宣告temp,並將引數向下造型。RTTI雖然程式碼簡潔,但使用分支語句不夠優雅。另外,①程式設計師還要注意,具體型別判斷在前;②RTTI將佔用較多的執行時間和空間。
《Java程式設計思想》中,有句話
Java中除了static方法和final方法(private方法屬於final方法)之外,其他所有方法都是後期繫結,也就是執行時繫結,我們不必判斷是否應該進行後期繫結-它會自動發生。
這裡提到的後期繫結,也只是針對引數的宣告型別來選擇具體的方法。
依賴設計模式實現雙分派
既然Java支援a.m(b)時,按a的具體型別繫結相應的方法,那如果通過某種方式在a.m(b)的實現中,完成了b.m1()的呼叫,那不就實現“雙分派”了嗎?縱覽GOF23,有兩種設計模式完美地支援這一種,分別是命令模式和訪問者模式
命令模式實現雙分派
原始碼
命令模式的UML圖
抽象receiver
public abstract class Receiver {
abstract void doSth();
}
複製程式碼
ConcreteReceiver1
public class ConcreteCommand1 extends Command {
private Receiver receiver;
public ConcreteCommand1(Receiver receiver) {
this.receiver = receiver;
}
@Override
void execute(Receiver receiver) {
System.out.println("我是command1,入參是Receiver");
receiver.doSth();
}
@Override
void execute(ConcreteReceiver1 receiver) {
System.out.println("我是command1,入參是ConcreteReceiver1");
receiver.doSth();
}
@Override
void execute(ConcreteReceiver2 receiver) {
System.out.println("我是command1,入參是ConcreteReceiver2");
receiver.doSth();
}
}
複製程式碼
抽象Command
public abstract class Command {
private Receiver receiver;
public Command(Receiver receiver) {
this.receiver = receiver;
}
abstract void execute(Receiver receiver);
abstract void execute(ConcreteReceiver1 receiver);
abstract void execute(ConcreteReceiver2 receiver);
public Receiver getReceiver() {
return receiver;
}
}
複製程式碼
ConcreteCommand1
public class ConcreteCommand1 extends Command {
public ConcreteCommand1(Receiver receiver) {
super(receiver);
}
@Override
public Receiver getReceiver() {
return super.getReceiver();
}
@Override
void execute(Receiver receiver) {
System.out.println("我是command1,入參是ConcreteReceiver2");
receiver.doSth();
}
}
複製程式碼
ConcreteCommand2
public class ConcreteCommand2 extends Command {
public ConcreteCommand2(Receiver receiver) {
super(receiver);
}
@Override
void execute(Receiver receiver) {
System.out.println("我是command2,入參是Receiver");
receiver.doSth();
}
@Override
void execute(ConcreteReceiver1 receiver) {
System.out.println("我是command2,入參是ConcreteReceiver1");
receiver.doSth();
}
@Override
void execute(ConcreteReceiver2 receiver) {
System.out.println("我是command2,入參是ConcreteReceiver2");
receiver.doSth();
}
@Override
public Receiver getReceiver() {
return super.getReceiver();
}
}
複製程式碼
Invoker
public class Invoker {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void act() {
this.command.execute(command.getReceiver());
}
}
複製程式碼
client
public class Client {
public static void main(String[] args) {
Invoker invoker = new Invoker();
Receiver receiver1 = new ConcreteReceiver1();
Receiver receiver2 = new ConcreteReceiver2();
Command command1 = new ConcreteCommand1(receiver1);
Command command2 = new ConcreteCommand2(receiver2);
invoker.setCommand(command1);
invoker.act();
}
}
複製程式碼
執行結果
我是command1,入參是Receiver
receiver1 處理命令1
複製程式碼
分析
為了湊a.m(b)這種格式,弄得很醜,大家見諒。Client中,Receiver和Command都是按介面宣告的,當執行到invoker.setCommand(command1); invoker.act();
時,程式走至
ConcreteCommand1.execute(Receiver);
,因此Java綁定了ConcreteCommand1中的第一個方法
我們看到的列印輸出就是我是command1,入參是Receiver
在這個方法中,receiver又成為了a.m(b)
中的a,因為Java又可以根據其實際型別進行方法繫結,因此跑到ConcreteReceiver1中,為了不要臉地硬捧a.m(b),這裡又羅哩羅嗦地寫了三個過載方法。這時b是ConcreteCommand1,因此找到過載方法
receiver1 處理命令1
最佳實踐
《設計模式之禪》也提到了我開篇的疑惑,Client為什麼要知曉Receiver的存在呢?事實上我們在實際工作中,沒有人真的那麼幹。引用一段書中的原文:
每一個模式到實際應用的時候都有一些變形,命令模式的Receiver在實際應用中一般都會被封裝掉(除非非常必要,例如撤銷處理),那是因為在專案中:約定的優先順序最高,每一個命令是對一個或多個Receiver的封裝,我們可以在專案中通過有意義的類名或命令名處理命令角色和接收者角色的耦合關係(這就是約定),減少高層模組(Client類)對低層模組(Receiver角色類)的依賴關係,提高系統整體的穩定性。因此,建議大家在實際的專案開發時採用封閉Receiver的方式(當然了,仁者見仁,智者見智),減少Client對Reciver的依賴。
訪問者模式
瞭解訪問者模式的朋友看到這肯定會說,這tm哪裡是命令模式啊,明明是披著命令模式皮的訪問者模式嘛!確實,為了想方設法說明雙分派,已經把命令模式搞變態了,不妨好好看下訪問者模式。舉《設計模式之禪》的例子
演員演電影角色,一個演員可以扮演多個角色,我們先定義一個影視中的兩個角色:功夫主角和白痴配角
public interface Role {
//演員要扮演的角色
}
public class KungFuRole implements Role {
//武功天下第一的角色
}
public class IdiotRole implements Role {
//一個弱智角色
}
複製程式碼
角色有了,我們再定義一個演員抽象類
public abstract class AbsActor {
//演員都能夠演一個角色
public void act(Role role){
System.out.println("演員可以扮演任何角色");
}
//可以演功夫戲
public void act(KungFuRole role){
System.out.println("演員都可以演功夫角色");
}
}
複製程式碼
很簡單,這裡使用了Java的過載,我們再來看青年演員和老年演員,採用覆寫的方式來 細化抽象類的功能
public class YoungActor extends AbsActor {
//年輕演員最喜歡演功夫戲
public void act(KungFuRole role){
System.out.println("最喜歡演功夫角色");
}
}
public class OldActor extends AbsActor {
//不演功夫角色
public void act(KungFuRole role){
System.out.println("年齡大了,不能演功夫角色");
}
}
複製程式碼
覆寫和過載都已經實現,我們編寫一個場景,
public class Client {
public static void main(String[] args) {
//定義一個演員
AbsActor actor = new OldActor();
//定義一個角色
Role role = new KungFuRole();
//開始演戲
actor.act(role);
actor.act(new KungFuRole());
}
}
複製程式碼
得到輸出結果
演員可以扮演任何角色
年齡大了,不能演功夫角色
複製程式碼
使用上節提到的介紹的方法,可以非常輕鬆地分析出雙分派的實現原理。
深層原理
大家可以發現,通過設計模式實現的雙分派,其實是“偽雙分派”,至少深層的原理,需要閱讀更多資料,等我讀完《深入理解Java虛擬機器器》後,會回來把這一節補上。
參考檔案
www.iteye.com/topic/11307… www.voidcn.com/article/p-d… 《設計模式之禪》 《Java程式設計思想》