Double Dispatch模式及其在iOS開發(fā)中實踐

前言

  • 引子
  • C++中的Double Dispatch實例
  • Java中的Double Dispatch實例
  • Objective-C中實現碰撞檢測用到的Visitor模式

引子

在一個太空大戰(zhàn)游戲中掘宪,導彈可以撞向飛船呵萨,也可能撞向行星怨酝,所以在碰撞檢測的時候就需要判斷碰撞的結果娘赴。假設游戲有四種物體:飛船,隕石播赁,行星,導彈,那么就產生了4*3/2+4種情形(一枚導彈撞上另一枚導彈)薪夕。這種排列組合計算出的結果會隨著物體種類N的增多爆炸性增長,如果這個時候還用一堆if-else來檢測碰撞赫悄,那真是Naive了原献。這時我們可以利用面向對象語言的多態(tài)性質來在程序運行時動態(tài)綁定,因為碰撞檢測是一種“雙向選擇”埂淮,所以我們需要double dispatch(雙分派)姑隅,Visitor模式就是double dispatch的一種應用。

DD模式適合于處理多個對象之間的相互作用倔撞。
假如不用DD模式的話讲仰,那么每個對象跟別的對象發(fā)生關系時,就必須辛辛苦苦的進行if…else…枚舉误窖,因為它并不知道對方是何神圣叮盘。
DD模式的引入解決了這個問題,其實說白了就是利用語言內置的虛函數機制來替你干活霹俺,把工作移交給編譯器去做了柔吼。

C++中的Double Dispatch實例

本節(jié)內容摘自這里
我們先從字面上去理解它吧,直觀地說丙唧,它指的是兩次dispatch愈魏。這里的dispatch指的是什么呢?舉個例子:

class Event
   {
       public:
           virtual void PrintName()
           {
                cout<<"我是通用事件"<<endl;           
           }
   }
   
   class KeyEvent:public Event
   {
      public:
           virtual void PrintName()
           {
                cout<<"我是按鍵事件"<<endl;           
           }
   }
   
   class ClickEvent:public Event
   {
       public:
           virtual void PrintName()
           {
                cout<<"我是單擊事件"<<endl;           
           }
   }

多態(tài)性是動態(tài)的,被調用的方法由對象的真正類型確定培漏,這個過程就被稱之為dispatch溪厘。
例如在C++中,每個對象都有一個虛函數表牌柄,當用基類的類型引用子類對象時畸悬,虛函數指針指向的是子類的虛函數表,調用的虛函數都是子類中的版本珊佣,所以下面代碼輸出的是:“我是按鍵事件”蹋宦,這就算是一次dispatch的過程,即根據對象類型來動態(tài)確定調用哪個函數的過程咒锻。

Event* pEvent = new KeyEvent();
pEvent->PrintName();

什么時候會用到兩次dispatch呢? 繼續(xù)往下看:

class EventRecorder
   {
       public:
           virtual void RecordEvent(Event* event)
           {
               cout<<"使用EventRecorder記錄通用事件"<< endl;           
           }
           
           virtual void RecordEvent(KeyEvent* event)
           {
               cout<<"使用EventRecorder記錄按鍵事件"<< endl;           
           }
           
           virtual void RecordEvent(ClickEvent* event)
           {
               cout<<"使用EventRecorder記錄單擊事件"<< endl;           
           }
   }
   
   class AdvanceEventRecorder:public EventRecorder
   {
       public:
           virtual void RecordEvent(Event* event)
           {
               cout<<"使用高級EventRecorder記錄通用事件"<< endl;           
           }
           
           virtual void RecordEvent(KeyEvent* event)
           {
               cout<<"使用高級EventRecorder記錄按鍵事件"<< endl;           
           }
           
           virtual void RecordEvent(ClickEvent* event)
           {
               cout<<"使用高級EventRecorder記錄單擊事件"<< endl;           
           }
   }

這兩個類中分別包含三個重載函數冷冗,多態(tài)是動態(tài)的,而函數重載則是靜態(tài)的惑艇,它在編譯時期就確定下來了蒿辙,所以,下面代碼片段的運行結果并不是我們所期望的:

EventRecorder* pRecorder = new AdvanceEventRecorder();
Event* pEvent = new KeyEvent();
pRecorder->RecordEvent(pEvent);

輸出內容為:使用高級EventRecorder記錄通用事件
實際上滨巴,在這個場景中思灌,我們期望調用的是:AdvanceEventRecorder::RecordEvent(KeyEvent* event)
下面我們使用Double Dispatch設計模式來達到上面的代碼片段的目的,在所有Event對象中增加下面的函數:

virtual void RecordEvent(EventRecorder* recorder)
{
   recorder->RecordEvent(this);
}

下面的代碼片段將輸出:使用高級EventRecorder記錄按鍵事件

EventRecorder* pRecorder = new AdvanceEventRecorder();
    Event* pEvent = new KeyEvent();
    pEvent->RecordEvent(pRecorder);

可以看出兢卵,第一次dispatch正確地找到了KeyEvent的RecordEvent(EventRecorder* recorder)习瑰,第二次dispatch找到了AdvanceEventRecorder的RecordEvent(KeyEvent* event)。 Visitor模式就是對Double Dispatch的應用秽荤,另外甜奄,在碰撞檢測算法中也會經常用到。

Java中的Double Dispatch實例

本節(jié)參考自這里
相對于C++中使用繼承來說窃款,Java提供的接口和函數重載讓Double Dispatch模式更容易實現

1 根據對象來選擇行為問題

public interface Event {
}
public class BlueEvent implements Event {
}
public class RedEvent implements Event {
}
public class Handler {
public void handle(Event event){
System.out.println("It is event");
}
public void handle(RedEvent event){
System.out.println("It is RedEvent");
}
public void handle(BlueEvent event){
System.out.println("It is BlueEvent");
}
}
public class Main {
public static void main(String[] args) {
Event evt=new BlueEvent();
new Handler().handle(evt);
}
}

你認為運行結果是什么呢课兄?
結果:It is event
是不是有點出乎意料,不是It is BlueEvent晨继,這是因為Overload并不支持在運行時根據參數的運行時類型來綁定方法烟阐,所以要執(zhí)行哪個方法是在編譯時就選定了的。

2 Double Dispatch Pattern

由于Java,C++及C#都具有上述局限紊扬,通常我們只能通過Switch或if結構來實現蜒茄,當然這種實現方式既不優(yōu)雅而且影響代碼的可維護性。
通過以下的Double Dispatch Pattern便可以優(yōu)雅的實現餐屎。

public interface Event {
public void injectHandler(EventHandler v);
}
public class BlueEvent implements Event {
public void injectHandler(EventHandler v) {
v.handle(this);
}
}
public class RedEvent implements Event {
public void injectHandler(EventHandler v) {
v.handle(this);
}
}
public class EventHandler {
public void handle(BlueEvent e){
System.out.println("It is BlueEvent");
}
public void handle(RedEvent e){
System.out.println("It is RedEvent");
}
}
public class Main {
public static void main(String[] args) {
Event evt=new BlueEvent();
evt.injectHandler(new EventHandler());
}
}

Objective-C中實現碰撞檢測用到的Visitor模式

雖然OC不支持函數重載檀葛,但是我們可以老老實實的用方法名來區(qū)分類似visitXXX的訪問方法,并利用OC其獨有的SEL類型可以很好的在運行時判斷該調用哪個方法


感謝kouky提供的iOS上碰撞檢測的Demo腹缩,這里他用到了Visitor模式
由于判斷物體類型是用一個32位掩碼來標記屿聋,所以這里不可避免的要用到if語句空扎,這不代表它不是動態(tài)綁定,因為if語句是在初始化方法+ (id)contactVisitorWithBody:(SKPhysicsBody *)body forContact:(SKPhysicsContact *)contact中其作用的润讥,只是為了判斷物體類型转锈,而不是判斷碰撞兩者的組合類型
可以參考例子ColorAtom

首先新建一個訪問者基本類ContactVisitor,其本質為對SKPhysicsBody和SKPhysicsContact對象的封裝楚殿,而SKPhysicsContact在本例中雖未用到(因為碰撞檢測后啥也沒干撮慨,只輸出了碰撞雙方name),但其保存著碰撞坐標等信息勒魔,也很重要甫煞。兩次dispatch都是在訪問者基本類實現的,而碰撞后具體操作則卸載了訪問者具體類(如AtomNodeContactVisitor)

#import <Foundation/Foundation.h>
#import <SpriteKit/SpriteKit.h>
@interface ContactVisitor : NSObject

@property (nonatomic,readonly, strong) SKPhysicsBody *body;
@property (nonatomic, readonly, strong) SKPhysicsContact *contact;

+ (id)contactVisitorWithBody:(SKPhysicsBody *)body forContact:(SKPhysicsContact *)contact;
- (void)visit:(SKPhysicsBody *)body;

@end

屬性body即為訪問者的SKPhysicsBody冠绢,而方法visit:的參數為被訪問者的SKPhysicsBody
contactVisitorWithBody:forContact:方法的作用是根據掩碼類型初始化對應類型的訪問者具體類

#import "ContactVisitor.h"
#import <objc/runtime.h>
#import "NodeCategories.h"
#import "AtomNodeContactVisitor.h"
#import "PlayFieldSceneContactVisitor.h"
@implementation ContactVisitor
+ (id)contactVisitorWithBody:(SKPhysicsBody *)body forContact:(SKPhysicsContact *)contact
{
    //第一次dispatch,通過node類別返回對應的實例
    if ((body.categoryBitMask&AtomCategory)!=0) {
        return [[AtomNodeContactVisitor alloc] initWithBody:body forContact:contact];
    }
    if ((body.categoryBitMask&PlayFieldCategory)!=0) {
        return [[PlayFieldSceneContactVisitor alloc] initWithBody:body forContact:contact];
    }
    else{
        return nil;
    }
}

- (id)initWithBody:(SKPhysicsBody *)body forContact:(SKPhysicsContact *)contact
{
    self = [super init];
    if (self) {
        _contact = contact;
        _body = body;
    }
    return self;
}

- (void)visit:(SKPhysicsBody *)body
{
    //第二次dispatch常潮,通過構造方法名來執(zhí)行對應方法
    // 生成node的名字弟胀,比如"AtomNode"
    NSString *bodyClassName = [NSString stringWithUTF8String:class_getName(body.node.class)];
    
    // 生成方法名,比如"visitAtomBody"
    NSMutableString *contactSelectorString = [NSMutableString stringWithFormat:@"visit"];
    [contactSelectorString appendString:bodyClassName];
    [contactSelectorString appendString:@":"];
    
    SEL selector = NSSelectorFromString(contactSelectorString);
    //判斷是否存在此方法
    if ([self respondsToSelector:selector]) {
        [self performSelector:selector withObject:body];
    }
    
}

以訪問者具體類以AtomNodeContactVisitor類為例喊式,它繼承自訪問者基本類ContactVisitor

#import "ContactVisitor.h"

@interface AtomNodeContactVisitor : ContactVisitor

/*Atom訪問了Atom孵户,同類碰撞*/
-(void) visitAtomNode:(SKPhysicsBody*) anotherAtomBody;
/*Atom訪問了邊界,也就是球撞墻上了*/
-(void) visitPlayFieldScene:(SKPhysicsBody*) playfieldBody;
@end

在處理碰撞后的visitXXX方法中岔留,將碰撞雙方的訪問者和被訪問者的關系輸出

#import "AtomNodeContactVisitor.h"
#import "AtomNode.h"
#import "PlayFieldScene.h"
@implementation AtomNodeContactVisitor
-(void) visitAtomNode:(SKPhysicsBody*) anotherAtomBody
{
    AtomNode *thisAtom = (AtomNode*)self.body.node;
    AtomNode *anotherAtom = (AtomNode*)anotherAtomBody.node;
    //處理碰撞后的結果
    NSLog(@"%@->%@",thisAtom.name,anotherAtom.name);
}
-(void) visitPlayFieldScene:(SKPhysicsBody*) playfieldBody
{
    AtomNode *atom = (AtomNode*)self.body.node;
    PlayFieldScene *playfield = (PlayFieldScene*) playfieldBody.node;
    NSLog(@"%@->%@",atom.name,playfield.name);
}
@end

下面建立被訪問者類夏哭,其本質就是對SKPhysicsBody的封裝,并接受Visitor的注入

#import <Foundation/Foundation.h>
#import "ContactVisitor.h"
@interface VisitablePhysicsBody : NSObject
@property (nonatomic, readonly, strong) SKPhysicsBody *body;

- (id) initWithBody:(SKPhysicsBody *)body;
- (void) acceptVisitor:(ContactVisitor *)visitor;

@end

關鍵的一步:在acceptVisitor:方法中調用訪問者的visit:方法

#import "VisitablePhysicsBody.h"

@implementation VisitablePhysicsBody
- (id)initWithBody:(SKPhysicsBody *)body
{
    self = [super init];
    if (self) {
        _body = body;
    }
    return self;
}

- (void)acceptVisitor:(ContactVisitor *)visitor
{
    [visitor visit:self.body];
}

@end

可能有人會有疑問献联,visit:方法穿入的參數類型永遠是SKPhysicsBody竖配,這哪里是動態(tài)綁定啊,其實是由于本例的特殊性里逆,碰撞檢測時區(qū)分物體類型不是靠SKPhysicsBody子類化來區(qū)分和綁定进胯,而是靠SKPhysicsBody類中的categoryBitMask屬性來區(qū)分,這也就免不了需要在ContactVisitor初始化的時候通過if語句來判斷具體初始化哪個子類
最后原押,在Scene實現SKPhysicsContactDelegate協(xié)議

#pragma mark SKPhysicsContactDelegate
-(void)didBeginContact:(SKPhysicsContact *)contact
{
    //A->B
    ContactVisitor *visitorA = [ContactVisitor contactVisitorWithBody:contact.bodyA forContact:contact];
    VisitablePhysicsBody *visitableBodyB = [[VisitablePhysicsBody alloc] initWithBody:contact.bodyB];
    [visitableBodyB acceptVisitor:visitorA];
    //B->A
    ContactVisitor *visitorB = [ContactVisitor contactVisitorWithBody:contact.bodyB forContact:contact];
    VisitablePhysicsBody *visitableBodyA = [[VisitablePhysicsBody alloc] initWithBody:contact.bodyA];
    [visitableBodyA acceptVisitor:visitorB];
    
}

物理老師總說力的作用時相互的胁镐,所以我們需要兩次訪問:A訪問B和B訪問A,但是這樣會調用兩次visitXXX方法诸衔,原則上這兩個邏輯上對稱的方法我們只需要實現其中一個就可以盯漂,但必須得像上面代碼一樣,A->B和B->A缺一不可笨农,因為碰撞的時候我們不知道bodyA和bodyB的類型就缆,也就無法判斷visitXXX方法是A->B時能調用還是B->A時能調用到
當然,你也可以兩個visit方法都實現磁餐,但只對visitor的node做操作违崇,或只對visitable的node操作阿弃,總之仁者見仁智者見智啦

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市羞延,隨后出現的幾起案子渣淳,更是在濱河造成了極大的恐慌,老刑警劉巖伴箩,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件入愧,死亡現場離奇詭異,居然都是意外死亡嗤谚,警方通過查閱死者的電腦和手機棺蛛,發(fā)現死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來巩步,“玉大人旁赊,你說我怎么就攤上這事∫我埃” “怎么了终畅?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長竟闪。 經常有香客問我离福,道長,這世上最難降的妖魔是什么炼蛤? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任妖爷,我火速辦了婚禮,結果婚禮上理朋,老公的妹妹穿的比我還像新娘絮识。我一直安慰自己,他們只是感情好暗挑,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布笋除。 她就那樣靜靜地躺著,像睡著了一般炸裆。 火紅的嫁衣襯著肌膚如雪垃它。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天烹看,我揣著相機與錄音国拇,去河邊找鬼。 笑死惯殊,一個胖子當著我的面吹牛酱吝,可吹牛的內容都是我干的。 我是一名探鬼主播土思,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼务热,長吁一口氣:“原來是場噩夢啊……” “哼忆嗜!你這毒婦竟也來了?” 一聲冷哼從身側響起崎岂,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤捆毫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后冲甘,有當地人在樹林里發(fā)現了一具尸體绩卤,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年江醇,在試婚紗的時候發(fā)現自己被綠了濒憋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡陶夜,死狀恐怖凛驮,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情条辟,我是刑警寧澤辐烂,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站捂贿,受9級特大地震影響,放射性物質發(fā)生泄漏胳嘲。R本人自食惡果不足惜厂僧,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望了牛。 院中可真熱鬧颜屠,春花似錦、人聲如沸鹰祸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛙婴。三九已至粗井,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間街图,已是汗流浹背浇衬。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留餐济,地道東北人耘擂。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像絮姆,于是被迫代替她去往敵國和親醉冤。 傳聞我的和親對象是個殘疾皇子秩霍,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355