Избегание instanceof в Java

Наличие цепочки операций "instanceof" считается "запахом кода". Стандартный ответ - "полиморфизм использования". Как мне это сделать в этом случае?

Существует ряд подклассов базового класса; ни один из них не находится под моим контролем. Аналогичная ситуация была бы с классами Java Integer, Double, BigDecimal и т.д.

if (obj instanceof Integer) {NumberStuff.handle((Integer)obj);}
else if (obj instanceof BigDecimal) {BigDecimalStuff.handle((BigDecimal)obj);}
else if (obj instanceof Double) {DoubleStuff.handle((Double)obj);}

У меня есть контроль над NumberStuff и т.д.

Я не хочу использовать много строк кода, где бы делали несколько строк. (Иногда я делаю HashMap, сопоставляя Integer.class с экземпляром IntegerStuff, BigDecimal.class с экземпляром BigDecimalStuff и т.д. Но сегодня я хочу что-то более простое.)

Мне бы хотелось что-то простое:

public static handle(Integer num) { ... }
public static handle(BigDecimal num) { ... }

Но Java просто не работает.

Я хочу использовать статические методы при форматировании. То, что я форматирование, является составным, где Thing1 может содержать массив Thing2s, а Thing2 может содержать массив Thing1s. У меня возникла проблема, когда я реализовал свои форматирования следующим образом:

class Thing1Formatter {
  private static Thing2Formatter thing2Formatter = new Thing2Formatter();
  public format(Thing thing) {
      thing2Formatter.format(thing.innerThing2);
  }
}
class Thing2Formatter {
  private static Thing1Formatter thing1Formatter = new Thing1Formatter();
  public format(Thing2 thing) {
      thing1Formatter.format(thing.innerThing1);
  }
}

Да, я знаю HashMap, и еще немного кода может это исправить. Но "экземпляр" кажется таким читабельным и удобным для сравнения. Есть что-то простое, но не вонючее?

Примечание добавлено 5/10/2010:

Оказывается, новые подклассы, вероятно, будут добавлены в будущем, и мой существующий код должен будет обработать их изящно. В этом случае HashMap on Class не будет работать, потому что класс не будет найден. Цепочка операторов if, начиная с наиболее специфических и заканчивая самой общей, вероятно, лучше всего:

if (obj instanceof SubClass1) {
    // Handle all the methods and properties of SubClass1
} else if (obj instanceof SubClass2) {
    // Handle all the methods and properties of SubClass2
} else if (obj instanceof Interface3) {
    // Unknown class but it implements Interface3
    // so handle those methods and properties
} else if (obj instanceof Interface4) {
    // likewise.  May want to also handle case of
    // object that implements both interfaces.
} else {
    // New (unknown) subclass; do what I can with the base class
}

Ответ 1

Вам может быть интересна эта запись из блога Steve Yegge Amazon: "когда сбой полиморфизма" . По существу он рассматривает такие случаи, когда полиморфизм вызывает больше проблем, чем решает.

Проблема в том, что для использования полиморфизма вы должны в этом случае сделать логику "обрабатывать" часть каждого класса "переключение", т.е. целое и т.д. Ясно, что это непрактично. Иногда даже логически невозможно разместить код. Он рекомендует "примерный" подход как меньшее из нескольких зол.

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

Ответ 2

Как указано в комментариях, шаблон посетителя будет хорошим выбором. Но без прямого контроля над целевым/акцептором/visitee вы не можете реализовать этот шаблон. Здесь один из способов шаблона посетителя, возможно, все еще можно использовать здесь, даже если у вас нет прямого контроля над подклассами с помощью оберток (например, с помощью Integer):

public class IntegerWrapper {
    private Integer integer;
    public IntegerWrapper(Integer anInteger){
        integer = anInteger;
    }
    //Access the integer directly such as
    public Integer getInteger() { return integer; }
    //or method passthrough...
    public int intValue() { return integer.intValue(); }
    //then implement your visitor:
    public void accept(NumericVisitor visitor) {
        visitor.visit(this);
    }
}

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

Ответ 3

Вместо огромного if вы можете поместить экземпляры, которые вы обрабатываете на карте (key: class, value: handler).

Если поиск по ключу возвращает null, вызовите специальный метод обработчика, который пытается найти соответствующий обработчик (например, вызывая isInstance() для каждого ключа на карте).

Когда обработчик найден, зарегистрируйте его под новым ключом.

Это делает общий случай быстрым и простым и позволяет обрабатывать наследование.

Ответ 4

Вы можете использовать отражение:

public final class Handler {
  public static void handle(Object o) {
    try {
      Method handler = Handler.class.getMethod("handle", o.getClass());
      handler.invoke(null, o);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  public static void handle(Integer num) { /* ... */ }
  public static void handle(BigDecimal num) { /* ... */ }
  // to handle new types, just add more handle methods...
}

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

Ответ 5

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

public abstract class StuffHandler {
   private StuffHandler next;

   public final boolean handle(Object o) {
      boolean handled = doHandle(o);
      if (handled) { return true; }
      else if (next == null) { return false; }
      else { return next.handle(o); }
   }

   public void setNext(StuffHandler next) { this.next = next; }

   protected abstract boolean doHandle(Object o);
}

public class IntegerHandler extends StuffHandler {
   @Override
   protected boolean doHandle(Object o) {
      if (!o instanceof Integer) {
         return false;
      }
      NumberHandler.handle((Integer) o);
      return true;
   }
}

а затем аналогично для ваших других обработчиков. Затем это случай скрепления StuffHandlers по порядку (наиболее специфичный для наименее специфичного, с конечным обработчиком "fallback" ), а ваш код для отправки - просто firstHandler.handle(o);.

(Альтернатива - вместо использования цепочки, просто иметь List<StuffHandler> в вашем классе диспетчера и пропустить ее через список, пока handle() не вернет true).

Ответ 6

Я думаю, что лучшим решением является HashMap с классом как ключом и Handler как значение. Обратите внимание, что решение на основе HashMap работает с постоянной алгоритмической сложностью θ (1), а цепочка запаха if-instanceof-else выполняется в линейной алгоритмической сложности O (N), где N - количество ссылок в цепочке if-instanceof-else (то есть количество различных классов, которые нужно обрабатывать). Таким образом, производительность решения на основе HashMap асимптотически выше в N раз, чем производительность if-instanceof-else цепного решения. Подумайте, что вам нужно обрабатывать разные потомки класса Message по-разному: Message1, Message2 и т.д. Ниже приведен фрагмент кода для обработки на основе HashMap.

public class YourClass {
    private class Handler {
        public void go(Message message) {
            // the default implementation just notifies that it doesn't handle the message
            System.out.println(
                "Possibly due to a typo, empty handler is set to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        }
    }
    private Map<Class<? extends Message>, Handler> messageHandling = 
        new HashMap<Class<? extends Message>, Handler>();

    // Constructor of your class is a place to initialize the message handling mechanism    
    public YourClass() {
        messageHandling.put(Message1.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message1
        } });
        messageHandling.put(Message2.class, new Handler() { public void go(Message message) {
            //TODO: IMPLEMENT HERE SOMETHING APPROPRIATE FOR Message2
        } });
        // etc. for Message3, etc.
    }

    // The method in which you receive a variable of base class Message, but you need to
    //   handle it in accordance to of what derived type that instance is
    public handleMessage(Message message) {
        Handler handler = messageHandling.get(message.getClass());
        if (handler == null) {
            System.out.println(
                "Don't know how to handle message of type %s : %s",
                message.getClass().toString(), message.toString());
        } else {
            handler.go(message);
        }
    }
}

Дополнительная информация об использовании переменных типа Class в Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html

Ответ 8

Я решил эту проблему, используя reflection (около 15 лет назад в эпоху до Generics).

GenericClass object = (GenericClass) Class.forName(specificClassName).newInstance();

Я определил один общий класс (абстрактный базовый класс). Я определил много конкретных реализаций базового класса. Каждый конкретный класс будет загружен с параметром className. Это имя класса определяется как часть конфигурации.

Базовый класс определяет общее состояние по всем конкретным классам, а конкретные классы изменят состояние путем переопределения абстрактных правил, определенных в базовом классе.

В то время я не знаю названия этого механизма, который известен как reflection.

В этой статье осталось несколько альтернатив: Map и enum кроме отражения.