93

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

Существует ряд подклассов базового класса; ни один из них не находится под моим контролем. Аналогичная ситуация была бы с классами 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 (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 
} 
+4

Я предлагаю [образец посетителя] [1]. [1]: http://en.wikipedia.org/wiki/Visitor_pattern – lexicore

+0

Является ли это цепью ifthen, против которой вы возражаете, или просто использованием «instanceof»? – Greg

+20

Шаблон посетителя требует добавления метода к целевому классу (например, Integer) - простой в JavaScript, жесткий на Java. Отличная модель при проектировании целевых классов; не так просто, пытаясь научить старый класс новым трюкам. –

ответ

47

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

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

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

+18

Полиморфизм не подводит. Скорее, Стив Йегг не может придумать шаблон посетителя, который является идеальной заменой для 'instanceof'. – Rotsor

+8

Я не вижу, как здесь помогает посетитель. Дело в том, что ответ OpinionatedElf на NewMonster не должен быть закодирован в NewMonster, но в OpinionatedElf. – DJClayworth

+1

Если «Монстр» создан для посещения, эльф может посетить его, чтобы извлечь всю необходимую информацию, тем самым сохраняя логику в классе «OpinionatedElf», не прибегая к опасным методам использования отражения. – Rotsor

20

Как указано в комментариях, шаблон посетителя будет хорошим выбором. Но без прямого контроля над целевым/акцептором/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 здесь плохой запах, особенно если он ограничен одним методом, и я бы с удовольствием его использовал (возможно, по моему собственному предложению выше). Как вы говорите, его вполне читаемый, типичный и поддерживаемый. Как всегда, держите его простым.

+0

Да,« Formatter »,« Composite »,« Different types »все швы, чтобы указать в направление посетителя. –

+2

Как вы определяете, какую оболочку вы собираетесь использовать? через ветвление if if? –

+1

Как указывает @fasttooth, это решение только сдвигает проблему. Вместо того, чтобы использовать 'instanceof' для вызова правильного метода' handle() ', теперь вам нужно будет использовать его для вызова конструктора' XWrapper' справа ... – Matthias

13

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

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

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

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

+0

+1 Я использовал этот подход при обработке кода, сгенерированного из XML-схем , или системы обмена сообщениями, где есть десятки типов объектов, переданных моему коду по существу нестандартным способом. – DNA

9

Вы можете рассмотреть the Chain of Responsibility pattern. Для вашего первого примера, например:

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).

13

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

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... 
} 

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

+26

Я бы сказал, что это пахнет еще больше, чем экземпляр оператора. Должен работать, хотя. –

+3

@Tim Büthe: По крайней мере, вам не нужно иметь дело с растущей цепочкой 'if then else', чтобы добавлять, удалять или изменять обработчики. Код менее хрупкий для изменений. Поэтому я бы сказал, что по этой причине он превосходит подход 'instanceof'. Во всяком случае, я просто хотел дать верную альтернативу. –

+0

достаточно справедливо, upvote. –

9

Я думаю, что лучшим решением является HashMap с ключом класса и обработчиком как значение. Обратите внимание, что решение на основе 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); 
     } 
    } 
} 

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

0

Я решил эту проблему с помощью reflection (около 15 лет назад в заранее дженерик эры).

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

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

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

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

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

+0

Любопытно, почему вы не сделали 'GenericClass'' интерфейсом'? – Ztyx

+0

У меня было общее состояние и поведение по умолчанию, которое должно быть общим для многих связанных объектов –