Избегание 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
Ответ 7
Просто подойдите к экземпляру. Все обходные пути кажутся более сложными. Вот сообщение в блоге, в котором говорится об этом: http://www.velocityreviews.com/forums/t302491-instanceof-not-always-bad-the-instanceof-myth.html
Ответ 8
Я решил эту проблему, используя reflection
(около 15 лет назад в эпоху до Generics).
GenericClass object = (GenericClass) Class.forName(specificClassName).newInstance();
Я определил один общий класс (абстрактный базовый класс). Я определил много конкретных реализаций базового класса. Каждый конкретный класс будет загружен с параметром className. Это имя класса определяется как часть конфигурации.
Базовый класс определяет общее состояние по всем конкретным классам, а конкретные классы изменят состояние путем переопределения абстрактных правил, определенных в базовом классе.
В то время я не знаю названия этого механизма, который известен как reflection
.
В этой статье осталось несколько альтернатив: Map
и enum
кроме отражения.