前言
- 引子
- 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操作阿弃,總之仁者見仁智者見智啦