Vermeiden von Instanzen in Java

102

Eine Kette von "Instanz von" Operationen wird als "Codegeruch" betrachtet. Die Standardantwort lautet "Polymorphismus verwenden". Wie würde ich das in diesem Fall machen?

Es gibt eine Reihe von Unterklassen einer Basisklasse. Keiner von ihnen ist unter meiner Kontrolle. Eine analoge Situation wäre mit den Java-Klassen Integer, Double, BigDecimal usw.

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

Ich habe die Kontrolle über NumberStuff und so weiter.

Ich möchte nicht viele Codezeilen verwenden, wo ein paar Zeilen reichen würden. (Manchmal mache ich eine HashMap, die Integer.class einer Instanz von IntegerStuff, BigDecimal.class einer Instanz von BigDecimalStuff usw. zuordnet. Aber heute möchte ich etwas Einfacheres.)

Ich möchte so etwas Einfaches:

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

Aber Java funktioniert einfach nicht so.

Ich möchte beim Formatieren statische Methoden verwenden. Die Dinge, die ich formatiere, sind zusammengesetzt, wobei ein Thing1 ein Array Thing2s und ein Thing2 ein Array von Thing1s enthalten kann. Ich hatte ein Problem, als ich meine Formatierer wie folgt implementierte:

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

Ja, ich kenne die HashMap und ein bisschen mehr Code kann das auch beheben. Aber die "Instanz von" scheint im Vergleich so lesbar und wartbar zu sein. Gibt es etwas Einfaches, aber nicht stinkendes?

Anmerkung hinzugefügt 10.05.2010:

Es stellt sich heraus, dass wahrscheinlich in Zukunft neue Unterklassen hinzugefügt werden und mein vorhandener Code sie ordnungsgemäß behandeln muss. Die HashMap für Klasse funktioniert in diesem Fall nicht, da die Klasse nicht gefunden wird. Eine Kette von if-Anweisungen, die mit den spezifischsten beginnen und mit den allgemeinsten enden, ist wahrscheinlich die beste:

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
quelle
4
Ich würde ein [Besuchermuster] [1] vorschlagen. [1]: en.wikipedia.org/wiki/Visitor_pattern
Lexicore
25
Für das Besuchermuster muss der Zielklasse eine Methode hinzugefügt werden (z. B. Integer) - einfach in JavaScript, schwer in Java. Hervorragendes Muster beim Entwerfen der Zielklassen; Nicht so einfach, wenn man versucht, einer alten Klasse neue Tricks beizubringen.
Mark Lutton
4
@lexicore: Markdown in Kommentaren ist begrenzt. Verwenden Sie [text](link)diese Option, um Links in Kommentaren zu veröffentlichen.
BalusC
2
"Aber Java funktioniert einfach nicht so." Vielleicht verstehe ich Dinge falsch, aber Java unterstützt das Überladen von Methoden (auch bei statischen Methoden) ganz gut ... es ist nur so, dass Ihren obigen Methoden der Rückgabetyp fehlt.
Powerlord
4
@ Powerlord Die Überlastungsauflösung ist zur Kompilierungszeit statisch .
Aleksandr Dubinsky

Antworten:

55

Dieser Eintrag aus Steve Yegges Amazon-Blog könnte Sie interessieren: "Wenn der Polymorphismus versagt" . Im Wesentlichen spricht er Fälle wie diesen an, in denen Polymorphismus mehr Probleme verursacht als löst.

Das Problem ist, dass Sie zur Verwendung des Polymorphismus die Logik des "Handles" Teil jeder "Schalt" -Klasse machen müssen - in diesem Fall Integer usw. Dies ist eindeutig nicht praktikabel. Manchmal ist es logischerweise nicht einmal der richtige Ort, um den Code einzufügen. Er empfiehlt den "Instanz of" -Ansatz als das geringere von mehreren Übeln.

Wie in allen Fällen, in denen Sie gezwungen sind, stinkenden Code zu schreiben, lassen Sie ihn in einer Methode (oder höchstens einer Klasse) zugeknöpft, damit der Geruch nicht austritt.

DJClayworth
quelle
22
Polymorphismus versagt nicht. Vielmehr erfindet Steve Yegge das Besuchermuster nicht, das der perfekte Ersatz dafür ist instanceof.
Rotsor
12
Ich sehe nicht, wie Besucher hier helfen. Der Punkt ist, dass die Antwort von OpinionatedElf auf NewMonster nicht in NewMonster, sondern in OpinionatedElf codiert werden sollte.
DJClayworth
2
Der Punkt des Beispiels ist, dass OpinionatedElf anhand der verfügbaren Daten nicht erkennen kann, ob es das Monster mag oder nicht. Es muss wissen, zu welcher Klasse das Monster gehört. Das erfordert entweder eine Instanz von oder das Monster muss irgendwie wissen, ob es dem OpinionatedElf gefällt. Der Besucher kommt daran nicht vorbei.
DJClayworth
2
@DJClayworth Das Besuchermuster umgeht dies , indem es der MonsterKlasse eine Methode hinzufügt , deren Aufgabe im Wesentlichen darin besteht, das Objekt einzuführen, z. B. "Hallo, ich bin ein Ork. Was denkst du über mich?". Der meinungsgebundene Elf kann dann Monster anhand dieser "Grüße" mit einem ähnlichen Code beurteilen bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }. Der einzige monsterspezifische Code ist dann class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }genug für jede Monsterinspektion ein für alle Mal.
Rotsor
2
In der Antwort von @Chris Knight finden Sie den Grund, warum Besucher in einigen Fällen nicht angewendet werden können.
James P.
20

Wie in den Kommentaren hervorgehoben, wäre das Besuchermuster eine gute Wahl. Ohne direkte Kontrolle über das Ziel / den Akzeptor / den Visitee können Sie dieses Muster jedoch nicht implementieren. Hier ist eine Möglichkeit, wie das Besuchermuster hier möglicherweise noch verwendet werden kann, obwohl Sie keine direkte Kontrolle über die Unterklassen haben, indem Sie Wrapper verwenden (am Beispiel 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);
    }
}

Natürlich kann das Einpacken einer Abschlussklasse als Eigengeruch angesehen werden, aber vielleicht passt es gut zu Ihren Unterklassen. Persönlich denke ich nicht, instanceofdass es hier so schlimm riecht, besonders wenn es auf eine Methode beschränkt ist und ich es gerne verwenden würde (wahrscheinlich über meinen eigenen Vorschlag oben). Wie Sie sagen, es ist gut lesbar, typsicher und wartbar. Wie immer, halte es einfach.

Chris Knight
quelle
Ja, "Formatierer", "Verbundwerkstoff", "Verschiedene Typen" zeigen alle Nähte in Richtung des Besuchers.
Thomas Ahle
3
Wie bestimmen Sie, welchen Wrapper Sie verwenden werden? durch eine if-Instanz der Verzweigung?
schneller Zahn
2
Wie @fasttooth hervorhebt, verschiebt diese Lösung nur das Problem. Anstatt instanceofdie richtige handle()Methode aufzurufen , müssen Sie jetzt den richtigen XWrapperKonstruktor aufrufen ...
Matthias
15

Anstelle eines riesigen ifkönnen Sie die von Ihnen behandelten Instanzen in eine Map einfügen (Schlüssel: Klasse, Wert: Handler).

Wenn die Suche nach Schlüssel zurückgegeben wird null, rufen Sie eine spezielle Handlermethode auf, die versucht, einen passenden Handler zu finden (z. B. indem Sie isInstance()jeden Schlüssel in der Karte aufrufen ).

Wenn ein Handler gefunden wird, registrieren Sie ihn unter dem neuen Schlüssel.

Dies macht den allgemeinen Fall schnell und einfach und ermöglicht es Ihnen, die Vererbung zu handhaben.

Aaron Digulla
quelle
+1 Ich habe diesen Ansatz verwendet, wenn ich Code verarbeitet habe, der aus XML-Schemas oder Messaging-Systemen generiert wurde, in denen Dutzende von Objekttypen vorhanden sind, die meinem Code im Wesentlichen nicht typsicher übergeben wurden.
DNA
13

Sie können Reflexion verwenden:

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

Sie können die Idee erweitern, Unterklassen und Klassen, die bestimmte Schnittstellen implementieren, generisch zu behandeln.

Jordão
quelle
34
Ich würde argumentieren, dass dies noch mehr riecht als die Instanz des Operators. Sollte aber funktionieren.
Tim Büthe
5
@ Tim Büthe: Zumindest müssen Sie sich nicht mit einer wachsenden if then elseKette auseinandersetzen, um Handler hinzuzufügen, zu entfernen oder zu ändern. Der Code ist weniger anfällig für Änderungen. Aus diesem Grund würde ich sagen, dass es dem instanceofAnsatz überlegen ist . Jedenfalls wollte ich nur eine gültige Alternative geben.
Jordão
1
Dies ist im Wesentlichen, wie eine dynamische Sprache mit der Situation umgehen würde, indem sie Enten tippt
DNA
@DNA: Wäre das nicht Multimethoden ?
Jordão
1
Warum iterieren Sie über alle Methoden, anstatt sie zu verwenden getMethod(String name, Class<?>... parameterTypes)? Sonst würde ich ersetzen ==mit isAssignableFromfür die Typprüfung des Parameters.
Aleksandr Dubinsky
9

Sie können das Muster der Verantwortungskette berücksichtigen . Für Ihr erstes Beispiel so etwas wie:

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

und dann ähnlich für Ihre anderen Handler. Dann müssen die StuffHandler der Reihe nach aneinander gereiht werden (am spezifischsten bis am wenigsten spezifisch, mit einem endgültigen 'Fallback'-Handler), und Ihr Despatcher-Code ist gerecht firstHandler.handle(o);.

(Eine Alternative besteht darin, anstatt eine Kette zu verwenden, nur eine List<StuffHandler>in Ihrer Dispatcher-Klasse zu haben und sie die Liste durchlaufen zu lassen, bis handle()true zurückgegeben wird.)

Cowan
quelle
9

Ich denke, dass die beste Lösung HashMap mit Class als Schlüssel und Handler als Wert ist. Beachten Sie, dass die HashMap-basierte Lösung in der konstanten algorithmischen Komplexität θ (1) ausgeführt wird, während die Geruchskette von if-instanceof-else in linearer algorithmischer Komplexität O (N) ausgeführt wird, wobei N die Anzahl der Glieder in der if-instanceof-else-Kette ist (dh die Anzahl der verschiedenen zu behandelnden Klassen). Daher ist die Leistung einer HashMap-basierten Lösung asymptotisch N-mal höher als die Leistung einer if-instanceof-else-Kettenlösung. Beachten Sie, dass Sie verschiedene Nachkommen der Nachrichtenklasse unterschiedlich behandeln müssen: Nachricht1, Nachricht2 usw. Unten finden Sie das Code-Snippet für die HashMap-basierte Behandlung.

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

Weitere Informationen zur Verwendung von Variablen vom Typ Class in Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html

Serge Rogatch
quelle
für eine kleine Anzahl von Fällen (wahrscheinlich höher als die Anzahl dieser Klassen für ein reales Beispiel) würde das if-else die Karte übertreffen, abgesehen davon, dass überhaupt kein
Heapspeicher verwendet wird
0

Ich habe dieses Problem mit reflection(vor etwa 15 Jahren in der Zeit vor Generika) gelöst .

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

Ich habe eine generische Klasse (abstrakte Basisklasse) definiert. Ich habe viele konkrete Implementierungen der Basisklasse definiert. Jede konkrete Klasse wird mit className als Parameter geladen. Dieser Klassenname wird als Teil der Konfiguration definiert.

Die Basisklasse definiert den gemeinsamen Status für alle konkreten Klassen, und konkrete Klassen ändern den Status, indem sie die in der Basisklasse definierten abstrakten Regeln überschreiben.

Zu diesem Zeitpunkt kenne ich den Namen dieses Mechanismus nicht, der als bekannt ist reflection.

In diesem Artikel sind nur wenige weitere Alternativen aufgeführt : Mapund enumabgesehen von der Reflexion.

Ravindra Babu
quelle
Einfach nur neugierig, warum hast du nicht machen GenericClassein interface?
Ztyx
Ich hatte einen gemeinsamen Zustand und ein Standardverhalten, das von vielen verwandten Objekten geteilt werden muss
Ravindra babu