Когда можно использовать instanceof?

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

В любом случае, некоторые из этих объектов расширяют абстрактный класс Collider и передаются в CollisionHandler. Класс и обработчик Collider позаботятся обо всем техническом, вовлеченном в столкновение, и просто попросят, чтобы объект реализовал функцию collidesWith(Collider c), и изменил себя, основываясь на том, с чем он столкнулся.

Объекты многих разных классов будут сталкиваться друг с другом и будут действовать очень по-разному в зависимости от того, с каким типом объектов они столкнулись, и его специфических атрибутов.

Похоже, идеальным решением будет использовать instanceof следующим образом:

class SomeNPC extends Collider{
    collidesWith(Collider c){
        if(c instanceof enemy){
            Fight it or run away depending on your attributes and theirs.
        }
        else if(c instanceof food){
            Eat it, but only if it's yellow.
        }
        else if(c instanceof BeamOfLight){
            Try to move towards its source.
        }
    }
}

Это на самом деле кажется законным местом, например. У меня просто плохое предчувствие. Например, как если бы goto имело смысл в определенной ситуации. Чувствуется ли дизайн кому-нибудь принципиально? Если так, что бы вы порекомендовали сделать для достижения того же поведения.

8 ответов

Решение

Традиционный ответ: используйте шаблон Visitor. Вы добавляете новый интерфейс,

interface Visitor {
     void visit(Enemy e);
     void visit(Food f);
     void visit(BeanOfLight bol);
}

и метод,

public void visit(Visitor v) {
    visitor.visit(this);
}

Каждый объект в вашей игре реализует visit метод, и каждое необходимое действие реализует Visitor интерфейс. Так что, как только действие visits объект, он вынужден выполнить действие, связанное с этим объектом.

Вы, конечно, можете быть более подробными и не полагаться на механизм диспетчеризации методов.

Обновление: возвращаясь к заголовку вопроса, его всегда можно использовать instanceof, Это ваш код, это ваш язык для использования. Проблема в том, что если в вашем коде много мест, где вы используете instanceofвы неизбежно пропустите один из них рано или поздно, так что ваш код молча потерпит неудачу без компилятора, который бы вам помог. Посетитель сделает вашу жизнь более болезненной во время кодирования, так как он будет заставлять вас внедрять интерфейс каждый раз, когда вы его меняете, везде. Но с другой стороны, вы не пропустите дело таким образом.

Обновление 2: пожалуйста, прочитайте обсуждение ниже. Посетитель, конечно, свяжет вас, и вы почувствуете себя скованными этим, как только у вас будет более десятка типов. Более того, если вам нужно отправлять события, например, столкновения, основываясь на типах двух или более объектов, ни один посетитель вам не поможет (нет instanceof, либо): вам нужно будет реализовать свою собственную таблицу последствий столкновения, которая бы отображала ваши комбинации типов на объект (я бы сказал, Strategy, но я боюсь, что обсуждение вырастет в десять раз), что бы знать, как бороться с этим конкретным столкновением.

Обязательная цитата Страуструпа: "Ничто не заменит: интеллект, опыт, вкус, трудолюбие".

Класс посетителей часто рекомендуется. С помощью Visitor вы реализуете метод посещения:

interface Visitor {
 void visit(Enemy e);
 void visit(Food f);
 void visit(BeanOfLight bol);
}

Но это на самом деле эквивалентно:

class SomeNPC extends Collider {
  public void collidesWith( Enemy enemy )
  public void collidesWith( Food food )
  public void collidesWith( Bullet bullet )
}

Оба из них имеют недостатки.

  1. Вы должны реализовать их все, даже если реакция вашего объекта одинакова в каждом случае
  2. Если вы добавляете объект нового типа для столкновения, вам нужно написать метод для реализации столкновения с ним для каждого отдельного объекта.
  3. Если один объект в вашей системе по-разному реагирует на 27 типов коллайдеров, но все остальные реагируют одинаково, вам все равно нужно написать 27 методов для каждого класса.

Иногда самый простой способ сделать это:

collidesWith(Object o) {
  if (o instanceof Balloon) {
    // bounce
  } else {
    //splat
  }

Его преимущество заключается в том, что он хранит информацию о том, как объект реагирует на вещи, которые он ударяет этим объектом. это также означает, что если у Balloon есть подклассы RedBalloon, BlueBalloon и т. д., мы не должны принимать это во внимание, как мы сделали бы с шаблоном посетителя.

Традиционный аргумент в пользу того, чтобы не использовать instanceof, состоит в том, что это не OO, и вы должны использовать полиморфизм. Однако вам может быть интересна эта статья Стива Йегге "Когда полиморфизм терпит неудачу", которая объясняет, почему instanceof иногда является правильным ответом.

Странно, что никто еще не опубликовал "не сломанную" реализацию шаблона посетителя. И я не имею в виду не полагаться на побочные эффекты посетителя. Для этого нам нужно, чтобы наши посетители возвращали какой-то результат (назовем его R):

interface ColliderVisitor<R> {
     R visit(Enemy e);
     R visit(Food f);
     R visit(BeanOfLight bol);
     R visit(SomeNpc npc);
}

Далее мы модифицируем accept принять новых посетителей:

interface Collider {
    <R> R accept(ColliderVisitor<R> visitor);
}

конкретные реализации коллайдера придется называть надлежащим visit метод, как это (я предполагаю, Food implements Collider, но это не обязательно)

class Food implements Collider {
    @Override
    <R> R accept(ColliderVisitor<R> visitor) {
        return visitor.visit(this);
    }
}

Теперь для реализации столкновений мы можем сделать что-то вроде этого:

class SomeNpcCollisionVisitor implements ColliderVisitor<Action> {
    SomeNpcCollisionVisitor(SomeNpc me) { this.me = me; }
    SomeNpc me;
    @Override
    Action visit(Enemy they) { 
        return fightItOrRunAway(me.attributes(), they.attributes());
    }
    @Override
    Action visit(Food f) {
        return f.colour()==YELLOW ? eat(f) : doNothing;
    }
    @Override
    Action visit(BeamOfLight l) {
        return moveTowards(l.source());
    }
    @Override
    Action visit(SomeNpc otherNpc) {
       // What to do here? You did not say! The compiler will catch this thankfully.
    }
}

class CollisionVisitor implements 
        ColliderVisitor<ColliderVisitor<Action>> { // currying anyone?

    @Override
    Action visit(Enemy they) { 
        return new EnemyCollisionVisitor(they); // what to do here?
    }
    @Override
    Action visit(Food f) {
        return new FoodCollisionVisitor(f); // what to do here?
    }
    @Override
    Action visit(BeamOfLight l) {
        return new BeamOfLightCollisionVisitor(l); // what to do here?
    }
    @Override
    Action visit(SomeNpc otherNpc) {
       return new SomeNpcCollisionVisitor(otherNpc);
    }
}

Action collide(Collider a, Collider b) {
    return b.accept(a.accept(new CollisionVisitor()));
}

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

class ColliderVisitorWithDefault<R> implements ColliderVisitor {
    final R def;
    ColliderVisitorWithDefault(R def) { this.def = def; }
    R visit(Enemy e) { return def; }
    R visit(Food f) { return def; }
    R visit(BeanOfLight bol) { return def; }
    R visit(SomeNpc npc) { return def; }
}

Вам также понадобится какой-то способ повторно использовать код для коллизий (Food, SomeNpc) и (SomeNpc, Food), но это выходит за рамки этого вопроса.

Если вы думаете, что это слишком многословно - это потому, что это так. На языках с сопоставлением с образцом это можно сделать в несколько строк (пример на Haskell):

data Collider = 
    Enemy <fields of enemy>
  | Food <fields of food>
  | BeanOfLight <fields>
  | SomeNpc <fields>

collide (SomeNpc npc) (Food f) = if colour f == YELLOW then eat npc f else doNothing
collide (SomeNpc npc) (Enemy e) = fightOrRunAway npc (npcAttributes npc) (enemyAttributes e)
collide (SomeNpc npc) (BeamOfLight bol) = moveTowards (bolSource bol)
collide _ _ = undefined -- here you can put some default behaviour

Вы можете использовать здесь шаблон посетителя, где подклассы Collider реализуют отдельный метод для каждого типа столкновения, с которым он может столкнуться. Таким образом, ваш метод может превратиться в:

class SomeNPC extends Collider {

    public void collidesWith( Enemy enemy ) {}

    public void collidesWith( Food food ) {}

    public void collidesWith( Bullet bullet ) {}

    public void doCollision( Collider c ) {
        if( c.overlaps( this ) ) {
            c.collidesWith( this );
        }
    }
}

Вы поняли идею. Странная вещь в вашей модели состоит в том, что базовый класс Collider должен знать обо всех потенциальных подклассах, чтобы определить метод для этого типа. Частично это связано с проблемой шаблона посетителя, но это также потому, что Collider объединен в Visitor. Я бы посоветовал искать разделение между посетителем и коллайдером, чтобы вы могли определить, как вы хотите себя вести, когда происходят столкновения. Что это означает для ваших коллайдеров, так это то, что они могут изменить свое поведение в зависимости от внутреннего состояния. Скажите, что они неуязвимы против обычного режима, скрыты или мертвы. Глядя на код клиента, это может быть:

collider1.getCollisionVisitor().doCollision( collider2 );
collider2.getCollisionVisitor().doCollision( collider1 );

По моему мнению, то, что вы набросали выше, является законным использованием instanceof и может быть более читабельным, чем использование системы Visitor, если каждый класс взаимодействует только с несколькими другими классами, как показано выше.

Проблема в том, что он может превратиться в страницы else-ifдля каждого из двадцати типов врагов. Но с instanceof вы можете избежать этого при некотором стандартном использовании полиморфизма (проверьте один Enemy Обучай всех врагов одинаково, даже если они Orcс или Dalekс или еще много чего).

Шаблон Visitor делает это намного сложнее. Наиболее работоспособным решением было бы иметь один класс верхнего уровня, от которого происходят все ваши игровые объекты, и определять методы collideWith() в этом классе для всех его подклассов, но затем иметь реализацию по умолчанию для каждого из них, просто вызывая collideWith() для супертип:

class GameObject {
   void collideWith(Orc orc) {
      collideWith((Enemy)orc);
   }

   void collideWith(Enemy enemy) {
      collideWith((GameObject)enemy);
   }

   ...

   void collideWith(GameObject object) { }
}

class SomeNPC extends GameObject {
   void collideWith(Orc orc) {
      // Handle special case of colliding with an orc
   }

   // No need to implement all the other handlers,
   // since the default behavior works fine.
}

Есть несколько способов справиться с этим:

  • перечисления для типов коллайдеров, немного некрасивые, но безотказные
  • диспетчеризация классов с обработчиками: т.е. Map<Class, CollisionHandler>вы выбираете CollisionHandler из переданного класса коллайдера (или типа enum) и вызываете processCollision(source), Каждый класс Colllider имеет свою собственную карту обработчиков.
  • На основании вышеизложенного вы также можете создать комбинацию типов коллайдеров и обработчиков записи, это что-то вроде Map<Pair<ColliderType>, CollisionHandler> для каждого нового типа столкновения вам нужно создать новый обработчик. Плюсом является то, что такие объявления могут быть внешними (внедрение зависимостей), поэтому новые NPC /Object могут быть добавлены вместе с обработчиками Collision.

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

Это может быть что-то, чтобы рассмотреть. С помощью instanceof может быть полезным, но еще один способ рассмотреть идею посетителя - создать класс Factory, который проверяет взаимное поле, чтобы определить тип посетителя.

Вам все еще нужно создавать новые классы для каждой реализации, но они направляются в абстрактный класс, который определяет метод

 public abstract class Qualifier{
 public abstract String type(); //...
 }

/**
 * QualifierFactory method.
 */
public static Qualifier getQualifier(Item sp, Config cf) {
    String type = cf.type();
    if (XQualifier.type().equals(type))
        return new XQualifier(sp, cf);
    else if (NQualifier.type().equals(type))
        return new NQualifier(sp, cf);
    else if (Tools.isNone(type) || NoneQualifier.type().equals(type))
        return new NoneQualifier(sp);
    else if (CSQualifier.type().equals(type))
        return new CSQualifier(sp, cf);//...
 }

Возвращенный объект может быть действием.

Это использование instanceof, которое заставит большинство людей съежиться. Вот полезная ссылка, которая может дать вам некоторое представление: http://www.javapractices.com/topic/TopicAction.do?Id=31

Вместо этого у Collider должен быть какой-то метод collide(), который каждый подкласс будет переопределять.

Другие вопросы по тегам