Unikanie wystąpienia w Javie

102

Łańcuch operacji „instanceof” jest uważany za „zapach kodu”. Standardowa odpowiedź brzmi „użyj polimorfizmu”. Jak bym to zrobił w tym przypadku?

Istnieje wiele podklas klasy bazowej; żaden z nich nie jest pod moją kontrolą. Analogiczna sytuacja miałaby miejsce w przypadku klas Java Integer, Double, BigDecimal itp.

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

Mam kontrolę nad NumberStuff i tak dalej.

Nie chcę używać wielu linii kodu, w których wystarczyłoby kilka linii. (Czasami tworzę HashMap mapujący Integer.class na instancję IntegerStuff, BigDecimal.class na instancję BigDecimalStuff itp. Ale dzisiaj chcę czegoś prostszego.)

Chciałbym coś tak prostego jak to:

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

Ale Java po prostu nie działa w ten sposób.

Chciałbym używać statycznych metod podczas formatowania. Rzeczy, które formatuję, są złożone, gdzie Thing1 może zawierać tablicę Thing2s, a Thing2 może zawierać tablicę Thing1s. Miałem problem, kiedy zaimplementowałem moje elementy formatujące w ten sposób:

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

Tak, znam HashMap i trochę więcej kodu też może to naprawić. Ale „instanceof” wydaje się tak czytelny i łatwy do utrzymania w porównaniu. Czy jest coś prostego, ale nie śmierdzącego?

Uwaga dodana 10.05.2010:

Okazuje się, że prawdopodobnie w przyszłości zostaną dodane nowe podklasy, a mój istniejący kod będzie musiał z wdziękiem sobie z nimi radzić. HashMap on Class nie zadziała w takim przypadku, ponieważ Class nie zostanie znaleziona. Łańcuch instrukcji if, zaczynający się od najbardziej szczegółowego i kończący się na najbardziej ogólnym, jest prawdopodobnie najlepszy:

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
źródło
4
Proponuję [wzorzec odwiedzających] [1]. [1]: en.wikipedia.org/wiki/Visitor_pattern
lexicore
25
Wzorzec Visitor wymaga dodania metody do klasy docelowej (np. Integer) - łatwa w JavaScript, trudna w Javie. Doskonały wzór przy projektowaniu klas docelowych; nie jest to takie łatwe, gdy próbuje się nauczyć starej klasy nowych sztuczek.
Mark Lutton
4
@lexicore: przeceny w komentarzach są ograniczone. Służy [text](link)do publikowania łączy w komentarzach.
BalusC
2
„Ale Java po prostu nie działa w ten sposób”. Może źle rozumiem rzeczy, ale Java obsługuje przeciążanie metod (nawet w przypadku metod statycznych) w porządku ... po prostu w powyższych metodach brakuje typu zwracanego.
Powerlord
4
@Powerlord Rozdzielczość przeciążenia jest statyczna w czasie kompilacji .
Aleksandr Dubinsky

Odpowiedzi:

55

Może zainteresuje Cię ten wpis z bloga Steve'a Yegge'a na Amazon: „kiedy polimorfizm zawodzi” . Zasadniczo zajmuje się takimi przypadkami, w których polimorfizm sprawia więcej problemów niż rozwiązuje.

Problem polega na tym, że aby użyć polimorfizmu, musisz uczynić logikę „obsługi” częścią każdej „przełączającej” klasy - tj. W tym przypadku Integer itp. Oczywiście nie jest to praktyczne. Czasami nawet logicznie nie jest to właściwe miejsce na umieszczenie kodu. Zaleca podejście „instancja” jako mniejsze zło.

Podobnie jak we wszystkich przypadkach, w których jesteś zmuszony pisać śmierdzący kod, trzymaj go zapiętym na guziki w jednej metodzie (lub co najwyżej jednej klasie), aby zapach nie wyciekł.

DJClayworth
źródło
22
Polimorfizm nie zawodzi. Raczej Steve Yegge nie wymyśla wzorca gościa, który jest idealnym zamiennikiem instanceof.
Rotsor
12
Nie widzę, jak gość tu pomaga. Chodzi o to, że odpowiedź OpinionatedElf na NewMonster nie powinna być kodowana w NewMonster, ale w OpinionatedElf.
DJClayworth
2
Chodzi o to, że OpinionatedElf nie może stwierdzić na podstawie dostępnych danych, czy lubi, czy nie lubi Potwora. Musi wiedzieć, do jakiej klasy należy potwór. Wymaga to albo instancji, albo Potwór musi w jakiś sposób wiedzieć, czy OpinatedElf to lubi. Gość tego nie obchodzi.
DJClayworth
2
Wzorzec @DJClayworth Visitor omija ten problem , dodając do Monsterklasy metodę , której zadaniem jest po prostu wprowadzenie obiektu, np. „Witaj, jestem orkiem. Co o mnie myślisz?”. Uparty elf może następnie oceniać potwory na podstawie tych „powitań”, używając kodu podobnego do bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }. Jedyny kod specyficzny dla potwora będzie wtedy class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }wystarczający do każdej inspekcji potwora raz na zawsze.
Rotsor
2
Zobacz odpowiedź @Chris Knight, aby dowiedzieć się, dlaczego w niektórych przypadkach nie można zastosować Visitor.
James P.
20

Jak podkreślono w komentarzach, wzorzec odwiedzających byłby dobrym wyborem. Ale bez bezpośredniej kontroli nad celem / akceptantem / odwiedzającym nie możesz zaimplementować tego wzorca. Oto jeden ze sposobów, w jaki wzorzec gości mógłby być nadal używany tutaj, mimo że nie masz bezpośredniej kontroli nad podklasami za pomocą opakowań (na przykładzie liczby całkowitej):

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

Oczywiście opakowanie ostatniej klasy można uznać za własny zapach, ale może dobrze pasuje do twoich podklas. Osobiście uważam, że nie ma instanceoftu aż tak nieprzyjemnego zapachu, zwłaszcza jeśli ogranicza się to do jednej metody i chętnie bym go użył (prawdopodobnie ponad moją własną sugestią powyżej). Jak mówisz, jest całkiem czytelny, bezpieczny i łatwy w utrzymaniu. Jak zawsze, nie komplikuj.

Chris Knight
źródło
Tak, „Formatter”, „Composite”, „Different types” wszystkie szwy wskazują kierunek odwiedzającego.
Thomas Ahle,
3
jak określić, którego opakowania będziesz używać? poprzez instancję rozgałęzienia if?
szybki ząb
2
Jak @fasttooth zwraca uwagę, to rozwiązanie tylko przesuwa problem. Zamiast używać instanceofdo wywoływania właściwej handle()metody, będziesz teraz musiał jej użyć, aby wywołać odpowiedni XWrapperkonstruktor ...
Matthias
16

Zamiast ogromnego if, możesz umieścić obsługiwane instancje w mapie (klucz: klasa, wartość: obsługa).

Jeśli wyszukiwanie według klucza powróci null, wywołaj specjalną metodę obsługi, która próbuje znaleźć pasującą procedurę obsługi (na przykład wywołując isInstance()każdy klucz w mapie).

Gdy program obsługi zostanie znaleziony, zarejestruj go pod nowym kluczem.

To sprawia, że ​​sprawa ogólna jest szybka i prosta, a także umożliwia obsługę dziedziczenia.

Aaron Digulla
źródło
+1 Użyłem tego podejścia podczas obsługi kodu generowanego ze schematów XML lub systemu przesyłania wiadomości, w którym istnieją dziesiątki typów obiektów, przekazywanych do mojego kodu w sposób zasadniczo niezabezpieczający.
DNA
13

Możesz użyć refleksji:

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

Możesz rozwinąć pomysł, aby ogólnie obsługiwać podklasy i klasy, które implementują określone interfejsy.

Jordão
źródło
35
Twierdzę, że pachnie to jeszcze bardziej niż instancja operatora. Powinien jednak działać.
Tim Büthe,
5
@Tim Büthe: Przynajmniej nie musisz zajmować się rosnącym if then elsełańcuchem, aby dodawać, usuwać lub modyfikować moduły obsługi. Kod jest mniej wrażliwy na zmiany. Więc powiedziałbym, że z tego powodu jest lepszy od instanceofpodejścia. Tak czy inaczej, chciałem tylko podać ważną alternatywę.
Jordão,
1
W zasadzie w ten sposób dynamiczny język poradziłby sobie z sytuacją, poprzez pisanie na klawiaturze
DNA,
@DNA: czy nie byłyby to metody multimetody ?
Jordão,
1
Dlaczego iterujesz wszystkie metody zamiast używać getMethod(String name, Class<?>... parameterTypes)? Albo chciałbym wymienić ==ze isAssignableFromna typ kontroli parametru.
Aleksandr Dubinsky
9

Możesz rozważyć wzorzec łańcucha odpowiedzialności . Na pierwszy przykład:

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

a potem podobnie dla twoich innych treserów. Następnie jest to przypadek łączenia ze sobą elementów StuffHandlers w kolejności (od najbardziej do najmniej szczegółowych, z końcową funkcją obsługi „rezerwowej”), a kod programu wysyłającego jest po prostu firstHandler.handle(o);.

(Alternatywą jest, zamiast używać łańcucha, po prostu mieć List<StuffHandler>w swojej klasie dyspozytora i zapętlić listę, dopóki nie handle()zwróci true).

Cowan
źródło
9

Myślę, że najlepszym rozwiązaniem jest HashMap z klasą jako kluczem i handlerem jako wartością. Zwróć uwagę, że rozwiązanie oparte na HashMap działa ze stałą złożonością algorytmiczną θ (1), podczas gdy łańcuch zapachu if-instanceof-else działa w liniowej złożoności algorytmicznej O (N), gdzie N to liczba łączy w łańcuchu if-instanceof-else (tj. liczba różnych klas do obsługi). Zatem wydajność rozwiązania opartego na HashMap jest asymptotycznie wyższa N razy niż wydajność rozwiązania łańcuchowego if-instanceof-else. Weź pod uwagę, że musisz inaczej obsługiwać różne elementy potomne klasy Message: Message1, Message2 itd. Poniżej znajduje się fragment kodu do obsługi opartej na 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);
        }
    }
}

Więcej informacji na temat użycia zmiennych typu Class w Javie: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html

Serge Rogatch
źródło
dla niewielkiej liczby przypadków (prawdopodobnie większej niż liczba tych klas dla dowolnego rzeczywistego przykładu), jeśli-else osiągnie lepsze wyniki niż mapa, poza tym, że w ogóle nie będzie używać pamięci sterty
idelvall
0

Rozwiązałem ten problem za pomocą reflection(około 15 lat temu w erze pre Generics).

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

Zdefiniowałem jedną klasę generyczną (abstrakcyjną klasę bazową). Zdefiniowałem wiele konkretnych implementacji klasy bazowej. Każda konkretna klasa zostanie załadowana z parametrem nazwa_klasy. Ta nazwa klasy jest definiowana jako część konfiguracji.

Klasa bazowa definiuje wspólny stan wszystkich klas konkretnych, a klasy konkretne modyfikują stan, zastępując reguły abstrakcyjne zdefiniowane w klasie bazowej.

W tym czasie nie znam nazwy tego mechanizmu, który był znany jako reflection.

Jeszcze kilka alternatyw są wymienione w tym artykule : Mapa enumoprócz refleksji.

Ravindra babu
źródło
Po prostu ciekawi, dlaczego nie zrobić ? GenericClassinterface
Ztyx
Miałem wspólny stan i domyślne zachowanie, które musi być współdzielone przez wiele powiązanych obiektów
Ravindra babu
0

Dodaj metodę w BaseClass, która zwraca nazwę klasy. I zastąp metody konkretną nazwą klasy

public class BaseClass{
  // properties and methods
  public String classType(){
      return BaseClass.class.getSimpleName();
  }
}

public class SubClass1 extends BaseClass{
 // properties and methods
  @Override
  public String classType(){
      return SubClass1.class.getSimpleName();
  }
}

public class SubClass2 extends BaseClass{
 // properties and methods
  @Override
  public String classType(){
      return SubClass1.class.getSimpleName();
  }
}

Teraz użyj obudowy przełącznika w następujący sposób:

switch(obj.classType()){
    case SubClass1:
        // do subclass1 task
        break;
    case SubClass2:
        // do subclass2 task
        break;
}
Shahriar Miraj
źródło