Menghindari instanceof di Java

102

Memiliki rangkaian operasi "contoh" dianggap sebagai "bau kode". Jawaban standarnya adalah "gunakan polimorfisme". Bagaimana saya melakukannya dalam kasus ini?

Ada sejumlah subclass dari kelas dasar; tidak satupun dari mereka di bawah kendali saya. Situasi serupa adalah dengan kelas Java Integer, Double, BigDecimal dll.

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

Saya memiliki kendali atas NumberStuff dan seterusnya.

Saya tidak ingin menggunakan banyak baris kode di mana beberapa baris bisa digunakan. (Terkadang saya membuat pemetaan HashMap Integer.class ke instance IntegerStuff, BigDecimal.class ke instance BigDecimalStuff, dll. Tapi hari ini saya menginginkan sesuatu yang lebih sederhana.)

Saya ingin sesuatu yang sederhana seperti ini:

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

Tapi Java tidak berfungsi seperti itu.

Saya ingin menggunakan metode statis saat memformat. Hal-hal yang saya format adalah gabungan, di mana Thing1 dapat berisi array Thing2s dan Thing2 dapat berisi array Thing1s. Saya mengalami masalah ketika saya menerapkan format saya seperti ini:

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

Ya, saya tahu HashMap dan sedikit lebih banyak kode dapat memperbaikinya juga. Tapi "contoh" tampaknya begitu mudah dibaca dan dipelihara sebagai perbandingan. Adakah yang sederhana tapi tidak berbau?

Catatan ditambahkan pada 5/10/2010:

Ternyata subclass baru mungkin akan ditambahkan di masa mendatang, dan kode saya yang ada harus menanganinya dengan baik. HashMap di Kelas tidak akan berfungsi dalam kasus itu karena Kelas tidak akan ditemukan. Rantai pernyataan if, dimulai dengan yang paling spesifik dan diakhiri dengan yang paling umum, mungkin adalah yang terbaik:

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
sumber
4
Saya akan menyarankan [pola pengunjung] [1]. [1]: en.wikipedia.org/wiki/Visitor_pattern
lexicore
25
Pola Pengunjung memerlukan penambahan metode ke kelas target (Integer misalnya) - mudah di JavaScript, sulit di Java. Pola yang sangat baik saat merancang kelas sasaran; tidak begitu mudah saat mencoba mengajarkan trik baru Kelas lama.
Mark Lutton
4
@lexicore: penurunan harga dalam komentar dibatasi. Gunakan [text](link)untuk memposting link di komentar.
BalusC
2
"Tapi Java tidak berfungsi seperti itu." Mungkin saya salah paham, tetapi Java mendukung metode overloading (bahkan pada metode statis) baik-baik saja ... hanya saja metode Anda di atas tidak memiliki tipe kembalian.
Powerlord
4
@Powerlord Resolusi kelebihan beban bersifat statis pada waktu kompilasi .
Aleksandr Dubinsky

Jawaban:

55

Anda mungkin tertarik dengan entri ini dari blog Amazon Steve Yegge: "ketika polimorfisme gagal" . Pada dasarnya dia menangani kasus seperti ini, ketika polimorfisme menyebabkan lebih banyak masalah daripada menyelesaikannya.

Masalahnya adalah bahwa untuk menggunakan polimorfisme Anda harus membuat logika "menangani" bagian dari setiap kelas 'switching' - yaitu Integer dll. Dalam kasus ini. Jelas ini tidak praktis. Terkadang secara logis bahkan bukan tempat yang tepat untuk meletakkan kode. Dia merekomendasikan pendekatan 'instanceof' sebagai yang lebih kecil dari beberapa kejahatan.

Seperti semua kasus di mana Anda dipaksa untuk menulis kode yang berbau, biarkan kancingnya dalam satu metode (atau paling banyak satu kelas) sehingga baunya tidak bocor.

DJClayworth
sumber
22
Polimorfisme tidak gagal. Sebaliknya, Steve Yegge gagal menemukan pola Pengunjung, yang merupakan pengganti yang sempurna instanceof.
Rotsor
12
Saya tidak melihat bagaimana pengunjung membantu di sini. Intinya adalah bahwa respons OpinionatedElf ke NewMonster tidak boleh dikodekan di NewMonster, tetapi di OpinionatedElf.
DJClayworth
2
Inti dari contoh ini adalah OpinionatedElf tidak dapat membedakan dari data yang tersedia apakah ia menyukai atau tidak menyukai Monster tersebut. Itu harus tahu milik Kelas Monster itu. Itu membutuhkan salah satu contoh, atau Monster harus tahu dengan cara tertentu apakah OpinionatedElf menyukainya. Pengunjung tidak bisa menghindari itu.
DJClayworth
2
Pola @DJClayworth Pengunjung tidak mendapatkan sekitar bahwa dengan menambahkan metode untuk Monsterkelas, tanggung jawab yang pada dasarnya adalah untuk memperkenalkan objek, seperti "Halo, saya Orc. Apa pendapat Anda tentang saya?". Peri beropini kemudian dapat menilai monster berdasarkan "salam" ini, dengan kode yang mirip dengan bool visitOrc(Orc orc) { return orc.stench()<threshold; } bool visitFlower(Flower flower) { return flower.colour==magenta; }. Satu-satunya kode spesifik monster kemudian akan class Orc { <T> T accept(MonsterVisitor<T> v) { v.visitOrc(this); } }cukup untuk setiap inspeksi monster sekali dan untuk semua.
Rotsor
2
Lihat jawaban @Chris Knight untuk alasan mengapa Pengunjung tidak dapat diterapkan dalam beberapa kasus.
James P.
20

Seperti yang disorot di komentar, pola pengunjung akan menjadi pilihan yang baik. Tetapi tanpa kontrol langsung atas target / akseptor / visitee Anda tidak dapat menerapkan pola itu. Berikut salah satu cara pola pengunjung masih dapat digunakan di sini meskipun Anda tidak memiliki kontrol langsung atas subkelas dengan menggunakan pembungkus (mengambil Integer sebagai contoh):

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

Tentu saja, membungkus kelas terakhir mungkin dianggap baunya sendiri, tetapi mungkin cocok dengan subkelas Anda. Secara pribadi, saya tidak berpikir instanceofitu bau busuk di sini, terutama jika itu terbatas pada satu metode dan saya akan dengan senang hati menggunakannya (mungkin atas saran saya sendiri di atas). Seperti yang Anda katakan, ini cukup mudah dibaca, aman dan mudah dipelihara. Seperti biasa, tetap sederhana.

Chris Knight
sumber
Ya, "Pemformat", "Gabungan", "Jenis berbeda" semuanya mengarah ke arah pengunjung.
Thomas Ahle
3
bagaimana Anda menentukan pembungkus mana yang akan Anda gunakan? melalui sebuah contoh percabangan?
gigi cepat
2
Seperti yang ditunjukkan oleh @fasttooth, solusi ini hanya mengubah masalah. Alih-alih menggunakan instanceofuntuk memanggil handle()metode yang benar Anda sekarang harus menggunakannya panggilan XWrapperkonstruktor yang tepat ...
Matthias
15

Alih-alih yang sangat besar if, Anda bisa meletakkan instance yang Anda tangani di peta (key: class, value: handler).

Jika pencarian dengan kunci kembali null, panggil metode penangan khusus yang mencoba menemukan penangan yang cocok (misalnya dengan memanggil isInstance()setiap kunci di peta).

Ketika penangan ditemukan, daftarkan dengan kunci baru.

Ini membuat kasus umum cepat dan sederhana dan memungkinkan Anda menangani warisan.

Aaron Digulla
sumber
+1 Saya telah menggunakan pendekatan ini ketika menangani kode yang dihasilkan dari skema XML, atau sistem perpesanan, di mana terdapat lusinan jenis objek, diserahkan ke kode saya dengan cara yang pada dasarnya tidak aman.
DNA
13

Anda dapat menggunakan refleksi:

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

Anda dapat memperluas ide untuk menangani subclass dan class secara umum yang mengimplementasikan antarmuka tertentu.

Jordão
sumber
34
Saya berpendapat bahwa baunya lebih dari contoh operator. Seharusnya berhasil.
Tim Büthe
5
@Tim Büthe: Setidaknya Anda tidak harus berurusan dengan if then elserantai yang berkembang untuk menambah, menghapus atau memodifikasi penangan. Kode tidak terlalu rentan terhadap perubahan. Jadi saya akan mengatakan bahwa karena alasan ini lebih unggul dari instanceofpendekatannya. Bagaimanapun, saya hanya ingin memberikan alternatif yang valid.
Jordão
1
Ini pada dasarnya adalah bagaimana bahasa dinamis akan menangani situasi, melalui pengetikan bebek
DNA
@ DNA: bukankah itu multimetode ?
Jordão
1
Mengapa Anda mengulang semua metode daripada menggunakan getMethod(String name, Class<?>... parameterTypes)? Atau saya akan mengganti ==dengan isAssignableFromuntuk pemeriksaan tipe parameter.
Aleksandr Dubinsky
9

Anda dapat mempertimbangkan pola Rantai Tanggung Jawab . Untuk contoh pertama Anda, seperti:

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

dan juga untuk penangan Anda yang lain. Maka ini adalah kasus merangkai StuffHandlers secara berurutan (paling spesifik hingga paling tidak spesifik, dengan penangan 'fallback' terakhir), dan kode despatcher Anda hanyafirstHandler.handle(o); .

(Alternatifnya adalah, daripada menggunakan chain, hanya memiliki a List<StuffHandler>di kelas dispatcher Anda, dan mengulanginya melalui daftar sampai handle()mengembalikan true).

Cowan
sumber
9

Saya pikir solusi terbaik adalah HashMap dengan Kelas sebagai kunci dan Penangan sebagai nilai. Perhatikan bahwa solusi berbasis HashMap berjalan dalam kompleksitas algoritme yang konstan θ (1), sedangkan rantai penciuman if-instanceof-else berjalan dalam kompleksitas algoritmik linier O (N), di mana N adalah jumlah tautan dalam rantai if-instanceof-else (yaitu jumlah kelas berbeda yang akan ditangani). Jadi kinerja solusi berbasis HashMap secara asimtotik lebih tinggi N kali daripada kinerja solusi rantai if-instanceof-else. Pertimbangkan bahwa Anda perlu menangani turunan kelas Message yang berbeda secara berbeda: Message1, Message2, dll. Di bawah ini adalah potongan kode untuk penanganan berbasis 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);
        }
    }
}

Info lebih lanjut tentang penggunaan variabel tipe Class di Java: http://docs.oracle.com/javase/tutorial/reflect/class/classNew.html

Serge Rogatch
sumber
untuk sejumlah kecil kasus (mungkin lebih tinggi dari jumlah kelas tersebut untuk contoh nyata) if-else akan mengungguli peta, selain tidak menggunakan memori heap sama sekali
idelvall
0

Saya telah memecahkan masalah ini menggunakan reflection(sekitar 15 tahun yang lalu di era pra Generik).

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

Saya telah mendefinisikan satu Kelas Generik (kelas Basis abstrak). Saya telah mendefinisikan banyak implementasi konkret dari kelas dasar. Setiap kelas beton akan dimuat dengan className sebagai parameter. Nama kelas ini didefinisikan sebagai bagian dari konfigurasi.

Kelas dasar mendefinisikan keadaan umum di semua kelas beton dan kelas beton akan mengubah keadaan dengan menimpa aturan abstrak yang ditentukan dalam kelas dasar.

Saat itu, saya tidak tahu yang namanya mekanisme ini, yang lebih dikenal dengan reflection.

Beberapa alternatif lagi terdaftar dalam artikel ini : Mapdan enumselain refleksi.

Ravindra babu
sumber
Hanya ingin tahu, mengapa tidak Anda membuat GenericClasssebuah interface?
Ztyx
Saya memiliki keadaan umum dan perilaku default, yang harus dibagikan ke banyak objek terkait
Ravindra babu