Objective-C 中的访问者模式

Posted

技术标签:

【中文标题】Objective-C 中的访问者模式【英文标题】:Visitor Pattern in Objective-C 【发布时间】:2012-03-11 02:03:31 【问题描述】:

我一直在寻找在 Objective-C 中实现访问者设计模式的最佳方式。由于该语言不支持方法重载,因此在 Java 中可能找到的“传统”实现似乎是不可能的。

在我当前的实现中,我有一个访问者协议、一个访问者类和该访问者类的几个子类,以及要访问的各种对象。一旦被访问的对象接受了访问者,它们就会调用访问者的访问方法,并将自己作为参数传递。 visit 方法接受一个 id,然后对其进行类型转换并调用

[self performTasksOnObjectClass: (ObjectClass *)object];

作为 if/elseif/else 块的一部分。这些调用由相关的访问者子类接收,访问者在对象上执行它需要的任何任务。

还有比这更好的实现访问者模式的方法吗?我不喜欢在 if/elseif/else 块中使用 'isKindOfClass' 或 'isMemberOfClass' 调用。它只是看起来笨重和不雅。此外,以这种方式实现访问者方法是否仍然“值得”?被访问的对象仍然可以不知道访问者,但还有其他方法可以实现。

已经有人建议,委托或类集群可能是访问者模式更合适的替代方案。我很想看看你们的想法!

编辑:实际上我在子类中调用了不同命名的方法,我已经更清楚了。

【问题讨论】:

无论您最终选择哪种设计模式,我都一定会通读 Apple 关于 Cocoa 设计模式的文档。您可能会找到更适合您需求的模式。 developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/… 感谢 Reed 的链接 - 但我真的很想为 Visitor 提供一个优雅的解决方案,特别是因为它与 Composite 结合得非常好。 我正在阅读 Carlo Chung 的 Pro Objective-C 设计模式,我推荐它。 amazon.com/Pro-Objective-C-Design-Patterns-iOS/dp/1430233303。他在书中实现了访问者模式,就像您使用协议然后传递 -(void)acceptMarkVisitor:(id ) 访问者一样。他指出(转述)Objective-C 中访问者模式的一个显着缺点是访问者与目标类耦合。添加新节点还涉及更改协议。他建议在协议中包含一个 catch all visitObj:(id) obj 以备将来更改。 @RickiG 不幸的是,本书的 Apress 网站包含第 15 章 - 访客设计模式之一! 啊我现在明白了。你可以下载本书的源文件 - 在 TouchPainter 项目中,这是他制作的包含所有模式的大项目,有一个 MarkVisitor.h 协议和你可以从那里调查。 apress.com/9781430233305(希望可以直接链接到源,它是公开的)。 【参考方案1】:

您可以使用一些自省/反思来使其更清晰。您不能重载方法名称,但可以避免编写这样的 switch 语句:

- (void)performTasks:(id)object

    Class class = [object class];
    while (class && class != [NSObject class])
    
        NSString *methodName = [NSString stringWithFormat:@"perform%@Tasks:", class];
        SEL selector = NSSelectorFromString(methodName);
        if ([self respondsToSelector:selector])
        
            [self performSelector:selector withObject:object];
            return;
        
        class = [class superclass];
    
    [NSException raise:@"Visitor %@ doesn't have a performTasks method for %@", [self class], [object class]];

您的实际 performTasks 方法将命名如下:

- (void)performFooTasks:(Foo *)foo

    //tasks for objects of class Foo


- (void)performBarTasks:(Bar *)bar

    //tasks for objects of class Bar


etc...

注意:如果您使用 ARC,您会通过以这种方式从字符串创建选择器得到虚假警告,因为它无法在编译时告诉方法参数。您可以使用 #pragma 使这些警告静音,如下所示:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector:selector withObject:object];
#pragma clang diagnostic pop

【讨论】:

我喜欢这样,它可以在添加新的访问对象类型时节省更改方法。我会检查 self 是否执行选择器以避免潜在的运行时错误,但我认为这会导致更多警告!明天当我完全清醒时,我会看看 objc/message.h,我相信也会有类似的东西来隐藏那里的警告。谢谢,这很可能是我一直在寻找的答案:) 如果使用 if respondsToSelector,您不会收到警告。警告是因为 ARC 不确定在将“对象”传递给选择器时是否保留它,因为它在编译时不知道选择器是什么,因为它是在运行时构造的,如果你这样做,这不是问题只是检查该方法是否存在。如果方法没有返回值,我实际上并不是 100% 确定你会收到警告。 我已经更新了我的答案,以便它能够正确处理对象子类。这个解决方案的好处是,如果您访问作为 Animal 子类的 Dog,您可以使用 performDogTasks 或 performAnimalTask​​s 方法(或两者),并且它将根据传入的对象使用更具选择性的方法。那即使在具有方法重载的语言中也很难做到这一点,因为它们在编译时无法确定参数是哪个子类。 我现在有机会好好看看这个。这真的正是我所追求的,一个很好的答案。非常感谢:)【参考方案2】:

您可以采用以下方法将选择器映射到 objc 类型,然后让此实现为“动态重载”进行方法查找。这样的实现将为您的实际使用消除大部分噪音(请参阅演示 - 8 页以下):

我们的包括:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

我们的基本类型:

MONVisitorEntry 将 Class 与选择器相关联:

MONVisitorEntry.h:

@interface MONVisitorEntry : NSObject

+ (MONVisitorEntry *)newVisitorEntryWithType:(Class)inType selector:(SEL)inSelector;

- (id)visit:(id)target parameter:(id)parameter;

- (Class)type;

@end

MONVisitorEntry.m:

@implementation MONVisitorEntry

@private
  Class type;
  SEL selector;


- (id)initWithType:(Class)inType selector:(SEL)inSelector

  self = [super init];
  if (0 != self) 
    type = inType;
    selector = inSelector;
  
  return self;


- (NSString *)description

  return [NSString stringWithFormat:@"%@ - Type: %@ - SEL: %@", [super description], type, NSStringFromSelector(selector)];


- (NSUInteger)hash

  return (NSUInteger)type;


- (Class)type

  return type;


+ (MONVisitorEntry *)newVisitorEntryWithType:(Class)inType selector:(SEL)inSelector

  return [[self alloc] initWithType:inType selector:inSelector];


- (id)visit:(id)target parameter:(id)parameter

  return ([target methodForSelector:selector])(target, selector, parameter);


@end

MONVisitorMapMONVisitorEntry 对象的映射。这种类型没有类型安全——你应该重新引入它。

MONVisitorMap.h:

@interface MONVisitorMap : NSObject

- (void)addEntryWithType:(Class)inType selector:(SEL)inSelector;

/* perhaps you would prefer that inTarget is also held by self? in that case, you could also cache the IMPs for faster lookups. */
- (id)visit:(id)inTarget parameter:(id)inParameter;

@end

MONVisitorMap.m:

@implementation MONVisitorMap

@private
  NSMutableSet * entries;


- (id)init

  self = [super init];
  if (0 != self) 
    entries = [NSMutableSet new];
  
  return self;


- (NSString *)description

  return [[super description] stringByAppendingString:[entries description]];


- (void)addEntryWithType:(Class)inType selector:(SEL)inSelector

  [entries addObject:[MONVisitorEntry newVisitorEntryWithType:inType selector:inSelector]];


- (id)visit:(id)inTarget parameter:(id)inParameter parameterClass:(Class)inParameterClass

  MONVisitorEntry * entry = 0;
  for (MONVisitorEntry * at in entries) 
    if (inParameterClass == at.type) 
      entry = at;
    
  

  if (0 != entry) 
    return [entry visit:inTarget parameter:inParameter];
  

  Class superclass = class_getSuperclass(inParameterClass);
  if (0 == superclass) 
    assert(0 && "exhausted class hierarchy!");
    return 0;
  

  return [self visit:inTarget parameter:inParameter parameterClass:superclass];


- (id)visit:(id)inTarget parameter:(id)inParameter

  return [self visit:inTarget parameter:inParameter parameterClass:[inParameter class]];


@end

演示:

创建一些测试类型(在此处添加一些.m 文件):

@interface Animal : NSObject
@end
@implementation Animal
@end

@interface Dog : Animal
@end
@implementation Dog
@end

@interface Greyhound : Dog
@end
@implementation Greyhound
@end

@interface Boxer : Dog
@end
@implementation Boxer
@end

@interface Squirrel : Animal
@end
@implementation Squirrel
@end

@interface Tapir : Animal
@end
@implementation Tapir
@end

创建访问者:

MONZoo.h:

@interface MONZoo : NSObject
/* our abstract "visit" entry, which introduces type safety: */
- (void)exhibit:(Animal *)inAnimal;
@end

MONZoo.m:

@implementation MONZoo

@private
  MONVisitorMap * visitorMap;


static NSString * Message(NSString * inMessage, id inInstance) 
  return [NSString stringWithFormat:@"Message: \"%@\" -- Instance: %@", inMessage, inInstance];


// Here's where you implement a method for an animal:
- (id)visitAnimal:(Animal *)p  return Message(@"What animal is this?", p); 
- (id)visitDog:(Dog *)p  return Message(@"What's up, Dog!", p); 
- (id)visitGreyhound:(Greyhound *)p  return Message(@"Ohhhhh a Greyhound!!", p); 
- (id)visitTapir:(Tapir *)p  return Message(@"What does it cost to feed this?", p); 

// Here's where you map methods to animals:    
+ (MONVisitorMap *)newVisitorMap

  MONVisitorMap * map = [MONVisitorMap new];

  [map addEntryWithType:[Dog class] selector:@selector(visitDog:)];
  [map addEntryWithType:[Greyhound class] selector:@selector(visitGreyhound:)];
  [map addEntryWithType:[Tapir class] selector:@selector(visitTapir:)];
  [map addEntryWithType:[Animal class] selector:@selector(visitAnimal:)];
  /* omitting the Boxer (Dog) to demonstrate pseudo-overload */

  return map;


- (id)init

  self = [super init];
  if (0 != self) 
    visitorMap = [[self class] newVisitorMap];
  
  return self;


- (NSString *)description

  return [[super description] stringByAppendingString:[visitorMap description]];


- (void)exhibit:(Animal *)inAnimal

  NSLog(@"Visiting Exhibit: %@", [visitorMap visit:self parameter:inAnimal]);


@end

现在试试吧:

int main(int argc, const char * argv[]) 

  @autoreleasepool 
    MONZoo * zoo = [MONZoo new];

    NSLog(@"Hello, Zoo! -- %@", zoo);

    [zoo exhibit:[Dog new]];
    [zoo exhibit:[Greyhound new]];
    [zoo exhibit:[Squirrel new]];
    [zoo exhibit:[Tapir new]];
    [zoo exhibit:[Boxer new]];
  
  return 0;

这促成了我们的动物园之旅:

2012-02-21 04:38:58.360 Visitor[2195:403] Hello, Zoo! -- <MONZoo: 0x10440ed80><MONVisitorMap: 0x1044143f0>(
    <MONVisitorEntry: 0x104414e00> - Type: Dog - SEL: visitDog:,
    <MONVisitorEntry: 0x104410840> - Type: Greyhound - SEL: visitGreyhound:,
    <MONVisitorEntry: 0x104415150> - Type: Animal - SEL: visitAnimal:,
    <MONVisitorEntry: 0x104415130> - Type: Tapir - SEL: visitTapir:
)
2012-02-21 04:38:58.363 Visitor[2195:403] Visiting Exhibit: Message: "What's up, Dog!" -- Instance: <Dog: 0x7f9a29d00120>
2012-02-21 04:38:58.363 Visitor[2195:403] Visiting Exhibit: Message: "Ohhhhh a Greyhound!!" -- Instance: <Greyhound: 0x7f9a29e002c0>
2012-02-21 04:38:58.364 Visitor[2195:403] Visiting Exhibit: Message: "What animal is this?" -- Instance: <Squirrel: 0x104416470>
2012-02-21 04:38:58.364 Visitor[2195:403] Visiting Exhibit: Message: "What does it cost to feed this?" -- Instance: <Tapir: 0x7f9a29d00120>
2012-02-21 04:38:58.364 Visitor[2195:403] Visiting Exhibit: Message: "What's up, Dog!" -- Instance: <Boxer: 0x1044140a0>

注意事项:

自带错误检测功能;) 请原谅我写的太快了 请原谅缺少文档 使用 ARC 编译 当然,您可以根据自己的需要做出一些变化。 这可能不是该模式的最真实形式,但如果您喜欢这种方法,您可以使用此程序添加一两个方法来轻松引入。

【讨论】:

这与我的解决方案类似,但使用反射生成方法名称的优点是您不必像在此处所做的那样手动维护类名称->访问者方法的映射。通过命名约定编码可以产生一些非常优雅的 DRY 解决方案,这就是为什么 Apple 在 Cocoa 中大量使用它的原因(KVC 使用它来确定 setter/getter 名称,UIViewController 使用类名来自动选择 nib,ARC 使用它来确定一个方法是否返回一个自动释放的对象等)。 @NickLockwood 唯一真正相似的部分是类层次结构遍历(在我的答案中存在之后,您添加到答案中的一种技术)。地图(我的答案)将比字符串构建(你的答案)更快地查找,它更容易扩展、专业化和变异。地图缩放更好,更灵活。在较大的系统中维护字符串构建也可能很乏味。但是,字符串构建的简单性可能对小问题有好处。如果需要,该地图还可以轻松调整以支持 SEL 名称匹配 - 只需使用 Classes 的数组构建它。 我的意思是它的相似之处在于两种解决方案都提供了一种将通用函数“visitObject”自动映射到特定函数“visitTapir”的方法。我非常不同意在假设的可扩展性的基础上为可以用简单 DRY 解决方案解决的问题构建复杂的手动解决方案的方法,我认为优化开发时间通常是比尝试优化运行时性能更好的目标,除了非常专业的情况(图形例程的内部循环等),但我怀疑我们必须同意不同意。 很抱歉,如果我没有给予应有的信任就偷了你的类遍历想法。我承认你的回答确实让我意识到我应该添加这样一个功能,但我希望你会认识到我没有使用你的任何代码,或者复制你的算法。 @NickLockwood 我不介意和你聊天或讨论答案——但是圣经敲打很烦人=)你以后能把它保留在你的答案中吗? tia。

以上是关于Objective-C 中的访问者模式的主要内容,如果未能解决你的问题,请参考以下文章

23种设计模式中的访问者模式

设计模式趣说访问者模式,颇有些无奈之举

访问者模式行为模式

Android源码访问者模式---HtmlDocument

Android源码访问者模式---HtmlDocument

GOF23设计模式之访问者模式(visitor)