Evitando instanceof en Java

102

Tener una cadena de operaciones de "instancia de" se considera un "olor a código". La respuesta estándar es "utilizar polimorfismo". ¿Cómo lo haría en este caso?

Hay varias subclases de una clase base; ninguno de ellos está bajo mi control. Una situación análoga sería con las clases de Java Integer, Double, BigDecimal, etc.

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);}

Tengo control sobre NumberStuff y demás.

No quiero usar muchas líneas de código donde bastaría con unas pocas líneas. (A veces hago una asignación HashMap de Integer.class a una instancia de IntegerStuff, BigDecimal.class a una instancia de BigDecimalStuff, etc. Pero hoy quiero algo más simple).

Me gustaría algo tan simple como esto:

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

Pero Java simplemente no funciona de esa manera.

Me gustaría usar métodos estáticos al formatear. Las cosas que estoy formateando son compuestas, donde un Thing1 puede contener una matriz Thing2s y una Thing2 puede contener una matriz de Thing1s. Tuve un problema cuando implementé mis formateadores de esta manera:

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);
  }
}

Sí, sé que HashMap y un poco más de código también pueden solucionarlo. Pero la "instancia de" parece tan legible y fácil de mantener en comparación. ¿Hay algo simple pero que no huele mal?

Nota agregada el 5/10/2010:

Resulta que probablemente se agregarán nuevas subclases en el futuro, y mi código existente tendrá que manejarlas con elegancia. El HashMap on Class no funcionará en ese caso porque no se encontrará la clase. Una cadena de declaraciones if, comenzando con la más específica y terminando con la más general, es probablemente la mejor después de todo:

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
}
Mark Lutton
fuente
4
Sugeriría un [patrón de visitante] [1]. [1]: en.wikipedia.org/wiki/Visitor_pattern
lexicore
25
El patrón de visitante requiere agregar un método a la clase de destino (Integer, por ejemplo): fácil en JavaScript, difícil en Java. Excelente patrón al diseñar las clases objetivo; no es tan fácil cuando se intenta enseñar nuevos trucos a una vieja Clase.
Mark Lutton
4
@lexicore: la rebaja en los comentarios es limitada. Úselo [text](link)para publicar enlaces en los comentarios.
BalusC
2
"Pero Java simplemente no funciona de esa manera". Tal vez estoy entendiendo mal las cosas, pero Java admite la sobrecarga de métodos (incluso en métodos estáticos) muy bien ... es solo que a sus métodos anteriores les falta el tipo de retorno.
Powerlord
4
@Powerlord La resolución de sobrecarga es estática en tiempo de compilación .
Aleksandr Dubinsky

Respuestas:

55

Quizás le interese esta entrada del blog de Amazon de Steve Yegge: "cuando falla el polimorfismo" . Esencialmente, está abordando casos como este, cuando el polimorfismo causa más problemas de los que resuelve.

El problema es que para usar polimorfismo tienes que hacer que la lógica de "manejar" sea parte de cada clase de "conmutación", es decir, Integer, etc. en este caso. Claramente, esto no es práctico. A veces, ni siquiera lógicamente es el lugar correcto para colocar el código. Él recomienda el enfoque de 'instancia de' como el menor de varios males.

Al igual que con todos los casos en los que se ve obligado a escribir código maloliente, manténgalo abotonado en un método (o como máximo en una clase) para que el olor no se escape.

DJClayworth
fuente
22
El polimorfismo no falla. Más bien, Steve Yegge no logra inventar el patrón Visitante, que es el reemplazo perfecto para instanceof.
Rotsor
12
No veo cómo el visitante ayuda aquí. El punto es que la respuesta de OpinionatedElf a NewMonster no debería estar codificada en NewMonster, sino en OpinionatedElf.
DJClayworth
2
El punto del ejemplo es que OpinionatedElf no puede decir a partir de los datos disponibles si le gusta o no le gusta el Monstruo. Tiene que saber a qué clase pertenece el monstruo. Eso requiere una instancia de, o el Monstruo tiene que saber de alguna manera si al OpinionatedElf le gusta. El visitante no lo evita.
DJClayworth
2
@DJClayworth patrón del visitante no moverse que mediante la adición de un método de Monsterclase, la responsabilidad de que es básicamente para introducir el objeto, como "Hola, soy un orco. ¿Qué opinas de mí?". El elfo con opiniones puede juzgar a los monstruos basándose en estos "saludos", con un código similar a bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }. El único código específico de los monstruos será class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }suficiente para cada inspección de monstruos de una vez por todas.
Rotsor
2
Vea la respuesta de @Chris Knight para conocer la razón por la cual Visitor no se puede aplicar en algunos casos.
James P.
20

Como se destaca en los comentarios, el patrón de visitantes sería una buena opción. Pero sin un control directo sobre el objetivo / aceptor / visitante no puede implementar ese patrón. Aquí hay una forma en que el patrón de visitante podría seguir usándose aquí aunque no tenga control directo sobre las subclases mediante el uso de envoltorios (tomando Integer como ejemplo):

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);
    }
}

Por supuesto, envolver una clase final podría considerarse un olor propio, pero tal vez sea una buena opción para tus subclases. Personalmente, no creo instanceofque sea un olor tan malo aquí, especialmente si se limita a un método y lo usaría felizmente (probablemente por encima de mi propia sugerencia anterior). Como dices, es bastante legible, seguro para escribir y fácil de mantener. Como siempre, hazlo simple.

Chris Knight
fuente
Sí, "Formateador", "Compuesto", "Diferentes tipos", todas las costuras apuntan en la dirección del visitante.
Thomas Ahle
3
¿Cómo se determina qué envoltorio va a utilizar? a través de una instancia if de ramificación?
diente rápido
2
Como señala @fasttooth, esta solución solo cambia el problema. En lugar de usar instanceofpara llamar al handle()método correcto , ahora tendrá que usarlo para llamar al XWrapperconstructor correcto ...
Matthias
15

En lugar de una enorme if, puede poner las instancias que maneja en un mapa (clave: clase, valor: manejador).

Si la búsqueda por clave regresa null, llame a un método de controlador especial que intenta encontrar un controlador que coincida (por ejemplo, llamando isInstance()a cada clave en el mapa).

Cuando se encuentra un controlador, regístrelo con la nueva clave.

Esto hace que el caso general sea rápido y simple y le permite manejar la herencia.

Aaron Digulla
fuente
+1 He usado este enfoque al manejar código generado a partir de esquemas XML o sistema de mensajería, donde hay docenas de tipos de objetos, entregados a mi código de una manera esencialmente no segura.
ADN
13

Puedes usar la reflexión:

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

Puede ampliar la idea para manejar genéricamente subclases y clases que implementan ciertas interfaces.

Jordão
fuente
34
Yo diría que esto huele aún más que la instancia del operador. Aunque debería funcionar.
Tim Büthe
5
@Tim Büthe: Al menos no tiene que lidiar con una if then elsecadena en crecimiento para agregar, eliminar o modificar controladores. El código es menos frágil a los cambios. Entonces yo diría que por esta razón es superior al instanceofenfoque. De todos modos, solo quería dar una alternativa válida.
Jordão
1
Así es esencialmente como un lenguaje dinámico manejaría la situación, a través de la tipificación de pato
DNA
@DNA: ¿No sería métodos múltiples ?
Jordão
1
¿Por qué iteras sobre todos los métodos en lugar de usarlos getMethod(String name, Class<?>... parameterTypes)? O de lo contrario, reemplazaría ==con isAssignableFromla verificación de tipo del parámetro.
Aleksandr Dubinsky
9

Podría considerar el patrón Cadena de responsabilidad . Para su primer ejemplo, algo como:

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

y luego de manera similar para sus otros manejadores. Entonces es un caso de encadenar los StuffHandlers en orden (del más específico al menos específico, con un controlador final 'alternativo'), y su código de despachador es solofirstHandler.handle(o); .

(Una alternativa es, en lugar de usar una cadena, simplemente tener un List<StuffHandler>en su clase de despachador y hacer que recorra la lista hasta que handle()devuelva verdadero).

Cowan
fuente
9

Creo que la mejor solución es HashMap con Class como clave y Handler como valor. Tenga en cuenta que la solución basada en HashMap se ejecuta en una complejidad algorítmica constante θ (1), mientras que la cadena de olor de if-instanceof-else se ejecuta en una complejidad algorítmica lineal O (N), donde N es el número de enlaces en la cadena if-instanceof-else (es decir, el número de clases diferentes que se manejarán). Por lo tanto, el rendimiento de la solución basada en HashMap es asintóticamente N veces mayor que el rendimiento de la solución de cadena if-instanceof-else. Considere que necesita manejar diferentes descendientes de la clase Message de manera diferente: Message1, Message2, etc. A continuación se muestra el fragmento de código para el manejo basado en 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);
        }
    }
}

Más información sobre el uso de variables de tipo Class en Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html

Serge Rogatch
fuente
para un pequeño número de casos (probablemente más alto que el número de esas clases para cualquier ejemplo real) el if-else superaría al mapa, además de no usar memoria de montón en absoluto
idelvall
0

He resuelto este problema usando reflection(alrededor de 15 años atrás en la era anterior a los genéricos).

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

He definido una clase genérica (clase base abstracta). He definido muchas implementaciones concretas de la clase base. Cada clase concreta se cargará con className como parámetro. Este nombre de clase se define como parte de la configuración.

La clase base define el estado común en todas las clases concretas y las clases concretas modificarán el estado anulando las reglas abstractas definidas en la clase base.

En ese momento, no sé el nombre de este mecanismo, que se ha conocido como reflection.

En este artículo se enumeran pocas alternativas más : Mapy enumademás de la reflexión.

Ravindra babu
fuente
Solo curiosidad, ¿por qué no hiciste GenericClassuna interface?
Ztyx
Tenía un estado común y un comportamiento predeterminado, que debe compartirse entre muchos objetos relacionados
Ravindra babu