Сложность дизайна с подклассами в Objective-C

Я использую Cocos2D для iOS, но вам, скорее всего, не нужно быть знакомым с Cocos2D, достаточно Obj-C, чтобы ответить на мой вопрос.

У меня есть класс врагов при разработке этой игры, которую я использовал в течение долгого времени, но теперь она достигла точки сложности, и мне нужно что-то сделать, чтобы сделать ее более организованной и читабельной.

В настоящее время это работает так: у меня есть вражеский класс, который я выделяю определенное количество раз и вставляю в изменяемый массив. Затем я могу пролистать этот изменяемый массив в любое время. Когда класс противника выделяется, ему также предписывается инициализировать и передать строку имени врага. В его инициализации есть серия операторов if/if else, которые проверяют имя врага и устанавливают для него правильные значения. Это работало просто отлично, за исключением того, что с точки зрения дизайна было очень сложно просматривать все эти имена, когда я добавлял все больше и больше врагов.

То, что я хочу сделать сейчас, это подкласс моего вражеского класса из всех разных врагов. Мне нужно будет получить доступ к свойствам врага, как и к другим видам врагов из этого класса.

Прямо сейчас во вражеском классе init у меня есть что-то вроде:

-(id) initWithEnemy:(NSString *)kind {

    if([kind isEqualToString:@"enemyName"]){

    //set values

    }
    else if([kind isEqualToString:@"anotherEnemyName"]){

    //set values

    }

    //etc, etc..

}

Теперь я хочу, чтобы эти заданные значения происходили в других файлах. Один или набор заголовочных/основных файлов для каждого врага. Итак, внутри initWithEnemy я подумал, может быть, я мог бы выделить класс имени врага из переданной «доброй» строки. Не уверен, что смогу использовать NSClassFromString. Я немного поэкспериментировал с ним, но я не совсем уверен, как получить доступ к свойствам класса, как я это делал раньше. Даже если я получил доступ к свойствам так же, как и раньше, означает ли это, что все классы имен врагов должны иметь одинаковое количество свойств?


person Chewie The Chorkie    schedule 31.01.2013    source источник
comment
Принадлежит codereview.stackexchange.com   -  person Sulthan    schedule 01.02.2013


Ответы (2)


Вы можете разделить своих врагов на абстрактный базовый класс с конкретными подклассами — это хороший подход. Однако помните, что часто лучше использовать композицию, а не наследование — именно здесь вы внедряете объекты в класс-держатель для моделирования чего-либо. В противном случае вы можете столкнуться с проблемой, когда у вас есть враг, который одновременно является «Монстром» и «Волшебником», а единственная цепочка наследования не позволяет этого.

Есть два шаблона проектирования, которые кажутся подходящими здесь — они оба сосредоточены на отделении сложных правил создания экземпляров от самого класса. Один из них – фабричный шаблон, а другой – шаблон конструктора. Если вы разбиваете иерархию классов, подойдет первое, в противном случае — второе.

Извините, больше примеров привести не могу - пишу это на iPad и на выходе.

person Jasper Blues    schedule 31.01.2013
comment
Это много информации для переваривания, но она кажется очень полезной. Мне нужно будет проработать это понемногу. Спасибо. - person Chewie The Chorkie; 01.02.2013
comment
@LearnCocos2D - отлично. . . не может просить намного больше, чем это. - person Jasper Blues; 01.02.2013

  1. Вместо строк объявите enum

    typedef enum {
        kEnemyInvalid = 0,
        kEnemyName1,
        kEnemyName2,
        [...]
    } EnemyType;
    
  2. Создайте класс Enemy с глобальными свойствами для всех типов врагов.

  3. Создайте необходимые подклассы врагов для каждого типа. Вполне возможно, что класс будет охватывать более одного типа врагов.
  4. Создайте функцию (возможно, метод класса)

    Class EnemyClassFromEnemyType(EnemyType type) {
        switch (type) {
            case kEnemyName1:
               return [EnemyName1 class];
            case kEnemyName2:
               return [EnemyName2 class];
            default:
               return Nil;
        }
    }
    

    Эта функция устанавливает связь между типом врага и классом, который его реализует. Есть способы сделать его более красивым, я предпочитаю использовать X-Macros.

  5. А теперь давайте создадим фабричный метод для создания врагов

    + (Enemy*)createEnemyWithType:(EnemyType*)enemyType {
        Class enemyClass = EnemyClassFromEnemyType(enemyType);
        return [[enemyClass alloc] initWithType:enemyType];
    }
    

То же самое с использованием X-Macros

Заголовочный файл

#define ENEMY_DEFINITIONS \
  ENEMY_DEFINITION(kEnemyInvalid, = 0, Nil) \
  ENEMY_DEFINITION(kEnemyName1,, [EnemyName1 class]) \
  ENEMY_DEFINITION(kEnemyName2,, [EnemyName2 class])

#define ENEMY_DEFINITION(name, intValue, enemyClass) name intValue, 

/**
 * Your enum declaration.
 */
typedef enum {
    ENEMY_DEFINITIONS
} EnemyType;

#undef ENEMY_DEFINITION

Class EnemyClassFromEnemyType(EnemyType type);
NSString* NSStringFromEnemyType(EnemyType type);

Файл реализации

#define ENEMY_DEFINITION(name, intValue, enemyClass) [name] = @#name,

NSString* EnemyTypeStringTable[] = {
    ENEMY_DEFINITIONS
}

#undef ENEMY_DEFINITION

NSString* NSStringFromEnemyType(EnemyType type) {
    return EnemyTypeStringTable[type]
}

#define ENEMY_DEFINITION(name, intValue, enemyClass) classTable[name] = enemyClass;

Class EnemyClassFromEnemyType(EnemyType type) {
    static Class* classTable = nil;

    if (classTable == nil) {
        classTable = malloc(sizeof(Class) * sizeof(EnemyTypeStringTable) / sizeof(NSString*));

        ENEMY_DEFINITIONS
    }

    return classTable[type];
}

#undef ENEMY_DEFINITION

Прелесть использования техники X-Macros заключается в том, что у вас есть все в одном месте, вы можете легко добавлять новые типы, ничего не меняя. Вы получаете что-то вроде перечислений Java, потому что перечисления могут иметь свойства.

person Sulthan    schedule 31.01.2013
comment
Спасибо, это предложение, которое я тоже получил ранее. Мне нужно будет много заменить NSStrings на перечисления. - person Chewie The Chorkie; 01.02.2013
comment
Шаг 4 и вообще меня смущает. Почему вы упомянули, что я могу поместить туда оператор switch? Я могу просто передать свой тип врага в метод, который я вызываю, верно? - person Chewie The Chorkie; 01.02.2013
comment
@VagueExplanation немного подробнее объяснил 4-й шаг. Эта функция используется заводским методом с шага 5. - person Sulthan; 01.02.2013
comment
На шаге 4 вы можете просто использовать NSClassFromString, верно? Таким образом, вам не нужно использовать оператор switch. - person Chewie The Chorkie; 01.02.2013
comment
@VagueExplanation Технически можно. Однако не рекомендуется с точки зрения качества кода. Что произойдет, если вы неправильно наберете строку? Вы получаете ошибку времени выполнения или с помощью obj-c вы, вероятно, получите несколько объектов nil, и вам придется отлаживать проблему. С этим дизайном вы получите ошибку компиляции. Кроме того, иногда вы хотите использовать один и тот же класс для разных типов врагов. - person Sulthan; 01.02.2013