Подтвердить что ты не робот

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

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

В любом случае некоторые из этих объектов расширяют абстрактный класс 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 yellow.
        }
        else if(c instanceof BeamOfLight){
            Try to move towards its source.
        }
    }
}

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

4b9b3361

Ответ 1

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

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, но боюсь, что обсуждение будет расти в десять раз), который знал бы, как справиться с этим конкретным столкновением.

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

Ответ 2

Часто рекомендуется использовать класс посетителей. С помощью 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 )
}

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

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

Иногда самый простой способ:

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

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

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

Ответ 3

Странно, что никто еще не опубликовал "не разбитую" модель шаблона посетителя. И не сломанный, я имею в виду не полагаться на побочные эффекты посетителя. Для этого нам нужно, чтобы наши посетители вернули некоторый результат (позвоните ему 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

Ответ 4

Здесь вы можете использовать шаблон посетителя, где подклассы 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 );

Ответ 5

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

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

Шаблон посетителя делает это намного сложнее. Наиболее эффективным решением будет состоять в том, чтобы иметь один класс верхнего уровня, из которого все ваши игровые объекты вытекают, и определить методы 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.
}

Ответ 6

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

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

Ответ 7

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

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

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

Ответ 8

Это может быть что-то для рассмотрения. Использование 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);//...
 }

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