Double Dispatch模式及其在iOS开发中实践

前言

  • 引子
  • C++中的Double Dispatch实例
  • Java中的Double Dispatch实例
  • Objective-C中实现碰撞检测用到的Visitor模式

引子

在一个太空大战游戏中,导弹可以撞向飞船,也可能撞向行星,所以在碰撞检测的时候就需要判断碰撞的结果。假设游戏有四种物体:飞船,陨石,行星,导弹,那么就产生了4*3/2+4种情形(一枚导弹撞上另一枚导弹)。这种排列组合计算出的结果会随着物体种类N的增多爆炸性增长,如果这个时候还用一堆if-else来检测碰撞,那真是Naive了。这时我们可以利用面向对象语言的多态性质来在程序运行时动态绑定,因为碰撞检测是一种“双向选择”,所以我们需要double dispatch(双分派),Visitor模式就是double dispatch的一种应用。

DD模式适合于处理多个对象之间的相互作用。
假如不用DD模式的话,那么每个对象跟别的对象发生关系时,就必须辛辛苦苦的进行if…else…枚举,因为它并不知道对方是何神圣。
DD模式的引入解决了这个问题,其实说白了就是利用语言内置的虚函数机制来替你干活,把工作移交给编译器去做了。

C++中的Double Dispatch实例

本节内容摘自这里
我们先从字面上去理解它吧,直观地说,它指的是两次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;           
           }
   }

多态性是动态的,被调用的方法由对象的真正类型确定,这个过程就被称之为dispatch。
例如在C++中,每个对象都有一个虚函数表,当用基类的类型引用子类对象时,虚函数指针指向的是子类的虚函数表,调用的虚函数都是子类中的版本,所以下面代码输出的是:“我是按键事件”,这就算是一次dispatch的过程,即根据对象类型来动态确定调用哪个函数的过程。

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

什么时候会用到两次dispatch呢? 继续往下看:

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;           
           }
   }

这两个类中分别包含三个重载函数,多态是动态的,而函数重载则是静态的,它在编译时期就确定下来了,所以,下面代码片段的运行结果并不是我们所期望的:

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实例

本节参考自这里
相对于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并不支持在运行时根据参数的运行时类型来绑定方法,所以要执行哪个方法是在编译时就选定了的。

2 Double Dispatch Pattern

由于Java,C++及C#都具有上述局限,通常我们只能通过Switch或if结构来实现,当然这种实现方式既不优雅而且影响代码的可维护性。
通过以下的Double Dispatch Pattern便可以优雅的实现。

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不支持函数重载,但是我们可以老老实实的用方法名来区分类似visitXXX的访问方法,并利用OC其独有的SEL类型可以很好的在运行时判断该调用哪个方法


感谢kouky提供的iOS上碰撞检测的Demo,这里他用到了Visitor模式
由于判断物体类型是用一个32位掩码来标记,所以这里不可避免的要用到if语句,这不代表它不是动态绑定,因为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,通过构造方法名来执行对应方法
    // 生成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,这哪里是动态绑定啊,其实是由于本例的特殊性,碰撞检测时区分物体类型不是靠SKPhysicsBody子类化来区分和绑定,而是靠SKPhysicsBody类中的categoryBitMask属性来区分,这也就免不了需要在ContactVisitor初始化的时候通过if语句来判断具体初始化哪个子类
最后,在Scene实现SKPhysicsContactDelegate协议

#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阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345