Шаблон посетителя в Objective-C

Я искал лучший способ реализации шаблона дизайна «Посетитель» в Objective-C. Поскольку язык не поддерживает перегрузку методов, «традиционная» реализация, такая как в Java, кажется невозможной.

В моей текущей реализации у меня есть протокол посетителя, класс посетителя и несколько подклассов этого класса посетителя, а также различные объекты для посещения. Как только посещаемый объект принимает посетителя, он вызывает метод посещения посетителя, передавая себя в качестве аргумента. Метод посещения принимает идентификатор, затем приводит его к типу и вызывает

[self performTasksOnObjectClass: (ObjectClass *)object];

как часть блока if/elseif/else. Эти вызовы принимаются соответствующим подклассом Посетителя, и Посетитель выполняет любые задачи, которые ему нужны для объекта.

Есть ли лучший способ реализации шаблона посетителя, чем этот? Мне не нравится прибегать к вызовам isKindOfClass или isMemberOfClass внутри блоков if/elseif/else. Это только кажется неуклюжим и неэлегантным. Кроме того, стоит ли по-прежнему реализовывать метод Visitor таким образом? Посещаемые объекты могут по-прежнему оставаться в неведении о Посетителе, но есть и другие способы добиться этого.

Уже было высказано предположение, что либо делегирование, либо кластеры классов могут быть более подходящими альтернативами шаблону «Посетитель». Мне было бы интересно узнать, что вы все думаете!

Редактировать: на самом деле у меня были методы с разными именами, вызываемые в подклассе, я сделал это более ясным.


person Simon Withington    schedule 20.02.2012    source источник
comment
Независимо от того, какой шаблон проектирования вы в конечном итоге выберете, я обязательно прочитаю документацию Apple по шаблонам проектирования Cocoa. Вы можете найти шаблон, который больше подходит для ваших нужд. developer.apple.com/library/ios /#documentation/Какао/Концептуальное/   -  person Reed Olsen    schedule 20.02.2012
comment
Reed, спасибо за ссылку, но меня очень интересует элегантное решение для Visitor, тем более, что оно прекрасно работает в сочетании с Composite.   -  person Simon Withington    schedule 20.02.2012
comment
Я читаю «Шаблоны проектирования Pro Objective-C» Карло Чанга, рекомендую. amazon.com/Pro-Objective-C-Design-Patterns- iOS/dp/1430233303. Он реализовал шаблон посетителя в книге так же, как вы сделали с протоколом, а затем передал -(void)acceptMarkVisitor:(id ‹MarkVisitor›) посетитель. Он отмечает (перефразируя), что заметным недостатком шаблонов посетителя в Objective-C является то, что посетитель связан с целевым классом. Также добавление новых узлов требует изменения протокола. Он предлагает включить в протокол все визитыОбж:(ид)объект для будущих изменений.   -  person RickiG    schedule 21.02.2012
comment
@RickiG, к сожалению, веб-сайт Apress для книги не содержит главу 15 - одну из шаблонов дизайна для посетителей!   -  person Peter M    schedule 21.02.2012
comment
Ааа, теперь я это вижу. Вы можете скачать исходные файлы для книги — в проекте TouchPainter, который является большим проектом, который он делает и включает в себя все шаблоны, есть протокол MarkVisitor.h, и вы можете исследовать его оттуда. apress.com/9781430233305 (надеюсь, можно напрямую ссылаться на источник, он общедоступен) .   -  person RickiG    schedule 21.02.2012
comment
@RickiG Вам, вероятно, следует превратить свой комментарий в ответ, даже если он предлагает то, что уже делает ОП.   -  person Peter M    schedule 21.02.2012
comment
обсуждение здесь: drdobbs.com/cpp/184410252, связанный вопрос: stackoverflow.com/questions/1334731/, обсуждение: cocoadev.com/index.pl?VisitorPattern   -  person Ray Tayek    schedule 21.02.2012


Ответы (2)


Вы можете использовать некоторую самоанализ/размышление, чтобы сделать это немного чище. Вы не можете перегружать имена методов, но вы можете избежать написания оператора 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
person Nick Lockwood    schedule 21.02.2012
comment
Мне это нравится, и это избавило бы от изменения метода при добавлении новых типов посещаемых объектов. Я бы добавил проверку того, выполняет ли self селектор, чтобы избежать потенциальных ошибок во время выполнения, но я полагаю, что это приведет к еще большему количеству предупреждений! Я посмотрю objc/message.h завтра, когда я как следует проснусь, я уверен, что там тоже будет аналогичный эквивалент, чтобы замять предупреждения. Спасибо, это вполне может быть ответом, который я искал :) - person Simon Withington; 21.02.2012
comment
Вы не получаете предупреждения об использовании if responsesToSelector. Предупреждение связано с тем, что ARC не уверен, сохранять ли объект, когда вы передаете его своему селектору, потому что он не знает, что такое селектор во время компиляции, поскольку он создается во время выполнения, что не является проблемой, если вы просто проверка существования метода. На самом деле я не уверен на 100%, что вы получите предупреждение, если метод не имеет возвращаемого значения. - person Nick Lockwood; 21.02.2012
comment
Я обновил свой ответ, чтобы он правильно обрабатывал подклассы объектов. Преимущество этого решения в том, что если вы посещаете Dog, который является подклассом Animal, у вас может быть либо метод PerformDogTasks, либо метод PerformAnimalTasks (или оба), и он будет использовать более избирательный метод в зависимости от переданного объекта. такое трудно сделать даже в языках с перегрузкой методов, потому что они не могут определить, какой подкласс является параметром во время компиляции. - person Nick Lockwood; 21.02.2012
comment
Я имел возможность взглянуть на это должным образом сейчас. Это действительно то, что мне нужно, отличный ответ. Большое спасибо :) - person Simon Withington; 21.02.2012

Вы можете использовать следующий подход для сопоставления селекторов с типами objc, а затем позволить этой реализации выполнять поиск методов для «динамической перегрузки». Такая реализация устранит большую часть шума для вашего реального использования (см. Демонстрацию — 8 страниц ниже):

Наш включает в себя:

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

Наши основные виды:

MONVisitorEntry связывает класс с селектором:

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

MONVisitorMap — это карта MONVisitorEntry объектов. этот тип не имеет безопасности типов - вы должны повторно ввести его.

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

МОНЗоо.м:

@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.
  • Конечно, есть варианты, которые вы можете сделать в соответствии с вашими потребностями.
  • Это может быть не самая верная форма шаблона, но вы можете легко представить это, добавив один или два метода с помощью этой программы, если вы предпочитаете такой подход.
person justin    schedule 21.02.2012
comment
Это похоже на мое решение, но преимущество создания имен методов с использованием отражения заключается в том, что вам не нужно вручную поддерживать карту имен классов-методов посетителей, как вы сделали здесь. Кодирование по соглашению об именах может привести к очень элегантным, СУХИМ решениям, поэтому Apple так активно использует его в Cocoa (KVC использует его для определения имен сеттера/геттера, UIViewController использует имя класса для автоматического выбора пера, ARC использует его для определить, возвращает ли метод автоматически выпущенный объект и т. д.). - person Nick Lockwood; 21.02.2012
comment
@NickLockwood Единственная действительно похожая часть - это обход иерархии классов (метод, который вы добавили в свой ответ после того, как он существовал в моем ответе). Карта (мой ответ) будет искать быстрее, чем построение строк (ваш ответ), ее легче расширять, специализировать и видоизменять. Карта лучше масштабируется и более гибкая. Построение строк также может быть утомительным в больших системах. Однако простота построения строк может быть полезна для небольших задач. При желании карту также можно легко адаптировать для поддержки SEL сопоставления имен — просто создайте ее, используя массив Classes. - person justin; 22.02.2012
comment
Я имел в виду, что это было похоже в том, что оба решения предоставляют способ автоматического сопоставления общей функции visitObject с конкретной функцией visitTapir. Я категорически не согласен с подходом создания сложных ручных решений проблем, которые можно решить с помощью простых СУХИХ решений на основе гипотетической расширяемости, и я бы сказал, что оптимизация времени разработки, как правило, является лучшей целью, чем попытка оптимизировать производительность во время выполнения. за исключением очень специальных случаев (внутренние циклы графических процедур и т. д.), но я подозреваю, что нам придется согласиться с несогласием. - person Nick Lockwood; 22.02.2012
comment
Извиняюсь, если мне показалось, что я украл вашу идею обхода классов, не отдав должного внимания. Я признаю, что ваш ответ заставил меня понять, что я должен добавить такую ​​​​функцию, но я надеюсь, вы поймете, что я не использовал ваш код и не дублировал ваш алгоритм. - person Nick Lockwood; 22.02.2012
comment
@NickLockwood, я не против поболтать с вами или обсудить ответы, но библейские удары раздражают =) не могли бы вы оставить свой ответ в будущем? Тиа. - person justin; 23.02.2012
comment
Извините, я просто не вижу в ваших комментариях много смысла, кроме: вы считаете мое решение хуже (ваше мнение), вы находите мое решение нежелательным (ваше мнение), оно не понравится хардкорным XP или кому бы то ни было (ваше мнение). методология) и список клише (слышал их!). Если вы хотите продвигать свою методологию, пусть ваш ответ станет форумом для этого. Я считаю грубым делать мой ответ форумом для этого - это тратит мое время и отвлекает от ответа. Без содержания я не могу поддерживать разговор. Я выскажу свое мнение, но не позволяйте этому заставить вас поверить, что вы мне не нравитесь. Ваше здоровье. - person justin; 23.02.2012
comment
@NickLockwood FWIW, я не против, что вы обновили свой ответ на основе моей техники. Лично я бы отдал должное ответу. Это место для обучения и обмена информацией, а не патентное бюро ;) - person justin; 23.02.2012
comment
([target methodForSelector:selector])(target, selector, parameter) почему бы не просто [target performSelector:selector withObject:parameter]? - person user102008; 05.06.2012