Eviter instanceof in Java
ayant une chaîne d'opérations" instanceof "est considéré comme une"odeur de code". La réponse standard est "utiliser le polymorphisme". Comment allais-je faire dans ce cas?
il existe un certain nombre de sous-classes d'une classe de base; aucune d'elles n'est sous mon contrôle. Une situation analogue se produirait avec les classes 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);}
j'ai le contrôle sur NumberStuff et ainsi de suite.
je ne veux pas utiliser de nombreuses lignes de code, où quelques lignes le ferait. (Parfois je fais un entier de cartographie HashMap.classe à une instance D'IntegerStuff, BigDecimal.classe à une instance de BigDecimalStuff etc. Mais aujourd'hui, je veux quelque chose de plus simple.)
j'aimerais quelque chose d'aussi simple que ceci:
public static handle(Integer num) { ... }
public static handle(BigDecimal num) { ... }
mais Java ne fonctionne pas de cette façon.
j'aimerais utiliser des méthodes statiques lors du formatage. Les choses que je formate sont composites, où un Thing1 peut contenir un tableau Thing2s et un Thing2 peut contenir un tableau de Thing1s. J'ai eu un problème quand j'ai implémenté mes formateuses comme ceci:
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);
}
}
Oui, je sais que la table de hachage et un peu plus de code peut résoudre ce problème aussi. Mais l '"instanceof" semble si lisible et maintenable par comparaison. Y a-t-il quelque chose de simple mais qui ne pue pas?
Note ajoutée le 5/10/2010:
il s'avère que de nouvelles sous-classes seront probablement ajoutées dans le futur, et mon code existant devra les gérer avec élégance. Le HashMap sur la classe ne fonctionnera pas dans ce cas car la classe ne sera pas trouvée. Une chaîne d'énoncés if, commençant par le plus spécifique et se terminant par le plus général, est probablement la meilleure après tout:
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
}
8 réponses
Vous pourriez être intéressé par cette entrée de Steve Yegge Amazon blog: "quand polymorphisme échoue" . En gros, il s'occupe de cas comme celui-ci, quand le polymorphisme cause plus de problèmes qu'il n'en résout.
le problème est que pour utiliser le polymorphisme vous devez faire la logique de" manipuler "partie de chaque classe de "commutation" - c.-à-d. entier etc. dans ce cas. Clairement, ce n'est pas pratique. Parfois, il n'est même pas logiquement le bon endroit pour mettre le code. Il recommande la "instanceof" comme étant le moindre de plusieurs maux.
comme dans tous les cas où vous êtes forcé d'écrire du code puant, gardez-le boutonné dans une méthode (ou au plus dans une classe) pour que l'odeur ne s'échappe pas.
tel que souligné dans les commentaires, le profil des visiteurs serait un bon choix. Mais sans contrôle direct sur la cible/accepteur / visiteur vous ne pouvez pas mettre en œuvre Ce modèle. Voici une façon dont le motif de visiteur pourrait éventuellement encore être utilisé ici, même si vous n'avez aucun contrôle direct sur les sous-classes en utilisant des enveloppes (en prenant entier comme exemple):
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);
}
}
bien sûr, emballer une dernière classe pourrait être considéré comme une odeur propre, mais peut-être que c'est une en bonne adéquation avec vos sous-classes. Personnellement, je ne pense pas que instanceof
est une mauvaise odeur ici, surtout si elle est confinée à une méthode et je serais heureux de l'utiliser (probablement sur ma propre suggestion ci-dessus). Comme vous le dites, son tout à fait lisible, tapesafe et maintenable. Comme toujours, garder les choses simples.
au lieu d'un énorme if
, vous pouvez placer les instances que vous manipulez dans une carte (clé: class, valeur: handler).
si la recherche par clé renvoie null
, appelez une méthode de handler spéciale qui essaie de trouver un handler correspondant (par exemple en appelant isInstance()
sur chaque clé de la carte).
quand un handler est trouvé, enregistrez-le sous la nouvelle clé.
cela rend le cas général rapide et simple et vous permet à l'héritage de la poignée.
vous pouvez utiliser la réflexion:
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...
}
vous pouvez développer l'idée de gérer de manière générique les sous-classes et les classes qui implémentent certaines interfaces.
Vous pourriez envisager de le modèle Chaîne de Responsabilité . Pour votre premier exemple, quelque chose comme:
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;
}
}
et de la même façon pour vos autres maîtres. Ensuite, il s'agit de mettre ensemble les manipulateurs dans l'ordre (le plus spécifique au moins spécifique, avec un gestionnaire de "repli" final), et votre code d'expéditeur est juste firstHandler.handle(o);
.
(une alternative est de, plutôt que d'utiliser une chaîne, juste avoir un List<StuffHandler>
dans votre classe de répartiteur, et le faire boucler dans la liste jusqu'à ce que handle()
retourne true).
je pense que la meilleure solution est HashMap avec Class as key et Handler as value. Notez que la solution basée sur HashMap s'exécute dans une complexité algorithmique constante θ (1), tandis que la chaîne smelling De if-instanceof-else s'exécute dans une complexité algorithmique linéaire O(N), où N est le nombre de liens dans la chaîne if-instanceof-else (c'est-à-dire le nombre de différentes classes à traiter). Ainsi, la performance de la solution basée sur HashMap est asymptotiquement plus élevée N fois que la performance de si-instanceof-else solution de chaîne. Considérer que vous avez besoin pour gérer les différents descendants de la classe de Message différemment: Message1, Message2, etc. . Ci-dessous se trouve l'extrait de code pour la manipulation basée sur 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);
}
}
}
plus d'informations sur l'utilisation des variables de la classe type en Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html
Juste aller avec le instanceof. Toutes les solutions semblent plus compliquées. Voici un billet de blog qui en parle: http://www.velocityreviews.com/forums/t302491-instanceof-not-always-bad-the-instanceof-myth.html
j'ai résolu ce problème en utilisant reflection
(il y a environ 15 ans à l'époque pré Generics).
GenericClass object = (GenericClass) Class.forName(specificClassName).newInstance();
j'ai défini une classe générique ( abstract Base class). J'ai défini de nombreuses implémentations concrètes de classe de base. Chaque classe de béton sera chargée avec className comme paramètre. Ce nom de classe est défini comme faisant partie de la configuration.
classe de Base définit l'état commun pour toutes les classes de béton et les classes de béton seront modifiez l'état en remplaçant les règles abstraites définies dans la classe de base.
à cette époque, je ne connais pas le nom de ce mécanisme, qui a été connu sous le nom de reflection
.
peu d'autres alternatives sont énumérées dans cet "article : Map
et enum
en dehors de la réflexion.