Bagaimana cara menghindari downcasting?

12

Pertanyaan saya adalah tentang kasus khusus Hewan kelas super.

  1. Saya Animalbisa moveForward()dan eat().
  2. Sealmeluas Animal.
  3. Dogmeluas Animal.
  4. Dan ada makhluk khusus yang juga Animaldisebut memanjang Human.
  5. Humanmengimplementasikan juga metode speak()(tidak diterapkan oleh Animal).

Dalam implementasi metode abstrak yang menerima Animalsaya ingin menggunakan speak()metode ini. Tampaknya tidak mungkin tanpa melakukan downcast. Jeremy Miller menulis dalam artikelnya bahwa orang yang tertekan berbau.

Apa yang akan menjadi solusi untuk menghindari downcasting dalam situasi ini?

Bart Weber
sumber
6
Perbaiki model Anda. Menggunakan hierarki taksonomi yang ada untuk memodelkan hierarki kelas biasanya merupakan ide yang salah. Anda harus menarik abstraksi Anda dari kode yang ada, tidak membuat abstraksi dan kemudian mencoba menyesuaikan kode itu.
Euforia
2
Jika Anda ingin Hewan berbicara, maka kemampuan berbicara adalah bagian dari apa yang membuat sesuatu menjadi hewan: artima.com/interfacedesign/PreferPoly.html
JeffO
8
maju kedepan? bagaimana dengan kepiting?
Fabio Marcolini
Item # manakah yang ada di Java Efektif, BTW?
goldilocks
1
Beberapa orang tidak dapat berbicara, jadi ada pengecualian jika Anda mencoba.
Tulains Córdova

Jawaban:

12

Jika Anda memiliki metode yang perlu tahu apakah kelas spesifik adalah tipe Humanuntuk melakukan sesuatu, maka Anda melanggar beberapa prinsip SOLID , terutama:

  • Prinsip terbuka / tertutup - jika, di masa depan, Anda perlu menambahkan jenis hewan baru yang dapat berbicara (misalnya, burung beo), atau melakukan sesuatu yang spesifik untuk jenis itu, kode Anda yang ada harus diubah
  • Prinsip pemisahan antarmuka - sepertinya Anda terlalu menyamaratakan. Seekor hewan dapat mencakup spektrum spesies yang luas.

Menurut pendapat saya, jika metode Anda mengharapkan tipe kelas tertentu, untuk memanggilnya metode tertentu, maka ubah metode itu untuk hanya menerima kelas itu, dan bukan antarmuka itu.

Sesuatu seperti ini :

public void MakeItSpeak( Human obj );

dan tidak seperti ini:

public void SpeakIfHuman( Animal obj );
BЈовић
sumber
Orang juga dapat membuat metode abstrak pada Animalpemanggilan canSpeakdan setiap implementasi konkret harus menentukan apakah ia dapat "berbicara" atau tidak.
Brandon
Tapi saya lebih suka jawaban Anda. Banyak kerumitan datang dari mencoba memperlakukan sesuatu seperti sesuatu yang bukan.
Brandon
@Brandon Saya kira, dalam hal ini, jika tidak bisa "berbicara", maka lontarkan pengecualian. Atau tidak melakukan apa-apa.
BЈовић
Jadi Anda mendapatkan kelebihan seperti ini: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}danpublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Bart Weber
1
Lakukan semuanya - makeItSpeak (hewan ISpeakingAnimal) - Anda kemudian dapat memiliki ISpeakingAnimal mengimplementasikan Hewan. Anda juga bisa membuat MakeSpeak (Hewan hewan) {berbicara jika instance dari ISpeakingAnimal}, tetapi itu memiliki bau yang samar.
ptyx
6

Masalahnya bukan bahwa Anda downcasting - itu yang Anda downcasting Human. Alih-alih, buat antarmuka:

public interface CanSpeak{
    void speak();
}

public abstract class Animal{
    //....
}

public class Human extends Animal implements CanSpeak{
    public void speak(){
        //....
    }
}

public void mysteriousMethod(Animal animal){
    //....
    if(animal instanceof CanSpeak){
        ((CanSpeak)animal).speak();
    }else{
        //Throw exception or something
    }
    //....
}

Dengan cara ini, syaratnya bukan bahwa hewan itu Human- syaratnya adalah ia dapat berbicara. Ini berarti mysteriousMethoddapat bekerja dengan subkelas non-manusia lainnya Animalselama mereka menerapkannya CanSpeak.

Idan Arye
sumber
dalam hal ini ia harus mengambil contoh dari CanSpeak sebagai ganti Animal
Newtopian
1
@Newtopian Berasumsi bahwa metode itu tidak memerlukan apa pun Animal, dan bahwa semua pengguna metode itu akan menyimpan objek yang ingin mereka kirim ke sana melalui CanSpeakreferensi tipe (atau bahkan Humantipe referensi). Jika itu masalahnya, metode itu bisa digunakan Humansejak awal dan kita tidak perlu memperkenalkannya CanSpeak.
Idan Arye
Menyingkirkan antarmuka tidak terkait dengan menjadi spesifik dalam tanda tangan metode, Anda dapat menyingkirkan antarmuka jika dan hanya jika hanya ada Manusia dan hanya akan ada manusia yang "CanSpeak".
Newtopian
1
@Newtopian Alasan kami memperkenalkan CanSpeakdi tempat pertama adalah bukan karena kami memiliki sesuatu yang mengimplementasikannya ( Human) tetapi kami memiliki sesuatu yang menggunakannya (metode). Maksudnya CanSpeakadalah untuk memisahkan metode itu dari kelas beton Human. Jika kita tidak memiliki metode yang memperlakukan hal-hal yang CanSpeakberbeda, tidak ada gunanya membedakan hal-hal itu CanSpeak. Kami tidak membuat antarmuka hanya karena kami memiliki metode ...
Idan Arye
2

Anda dapat menambahkan Berkomunikasi ke Hewan. Anjing menggonggong, Manusia berbicara, Anjing laut .. uhh .. Saya tidak tahu anjing laut apa.

Tetapi sepertinya metode Anda dirancang untuk jika (Hewan adalah Manusia) Bicara ();

Pertanyaan yang mungkin ingin Anda tanyakan adalah, apa alternatifnya? Sulit memberikan saran karena saya tidak tahu persis apa yang ingin Anda capai. Ada situasi teoretis di mana downcasting / upcasting adalah pendekatan terbaik.

Andrew Hoffman
sumber
15
Segel itu menjadi miliknya , tetapi rubah itu tidak terdefinisi.
2

Dalam hal ini, implementasi default speak()di AbstractAnimalkelas adalah:

void speak() throws CantSpeakException {
  throw new CantSpeakException();
}

Pada titik itu, Anda memiliki implementasi default di kelas Abstrak - dan itu berlaku dengan benar.

try {
  thingy.speak();
} catch (CantSeakException e) {
  System.out.println("You can't talk to the " + thingy.name());
}

Ya, ini berarti Anda punya try-catch yang tersebar di seluruh kode untuk menangani semuanya speak, tetapi alternatifnya adalah if(thingy is Human)membungkus semua yang berbicara.

Keuntungan dari pengecualian adalah bahwa jika Anda memiliki jenis hal lain di beberapa titik yang berbicara (burung beo) Anda tidak perlu menerapkan kembali semua tes Anda.


sumber
1
Saya tidak berpikir ini adalah alasan yang baik untuk menggunakan pengecualian (kecuali itu adalah kode python).
Bryan Chen
1
Saya tidak akan memilih karena ini secara teknis akan berhasil, tetapi saya benar-benar tidak suka desain semacam ini. Mengapa mendefinisikan metode pada tingkat induk yang tidak dapat diterapkan oleh induk ? Kecuali Anda tahu jenis objeknya (dan jika Anda melakukannya - Anda tidak akan memiliki masalah ini), Anda harus selalu menggunakan coba / tangkap untuk memeriksa pengecualian yang benar-benar dapat dihindari. Anda bahkan bisa menggunakan canSpeak()metode untuk menangani ini dengan lebih baik.
Brandon
2
Saya telah bekerja pada sebuah proyek yang memiliki banyak cacat yang berasal dari keputusan seseorang untuk menulis ulang banyak metode kerja untuk melemparkan pengecualian karena mereka menggunakan implementasi yang usang. Dia bisa memperbaiki implementasinya, tetapi memilih untuk melemparkan pengecualian di mana-mana. Secara alami, kami memasuki QA dengan ratusan bug. Jadi saya bias menolak melempar pengecualian dalam metode yang tidak seharusnya disebut. Jika mereka tidak seharusnya dipanggil, hapuslah .
Brandon
2
Alternatif untuk melemparkan pengecualian dalam kasus ini mungkin tidak melakukan apa-apa. Lagipula, mereka tidak dapat berbicara :)
BЈовић
@ Brandon ada perbedaan antara mendesainnya dari awal dengan pengecualian vs perkuatan di kemudian hari. Anda juga bisa melihat antarmuka dengan metode default di Java8, atau tidak melempar pengecualian dan hanya diam. Poin kuncinya adalah bahwa untuk menghindari keharusan downcast, fungsi perlu didefinisikan dalam jenis yang dilewatkan.
1

Downcasting terkadang perlu dan tepat. Secara khusus, itu sering tepat dalam kasus di mana seseorang memiliki objek yang mungkin atau mungkin tidak memiliki beberapa kemampuan, dan seseorang ingin menggunakan kemampuan itu ketika ada saat menangani objek tanpa kemampuan itu dalam beberapa mode standar. Sebagai contoh sederhana, anggap a Stringditanya apakah itu sama dengan beberapa objek arbitrer lainnya. Agar satu Stringsama dengan yang lain String, ia harus memeriksa panjang dan dukungan array karakter dari string lainnya. Jika Stringditanya apakah sama dengan Dog, bagaimanapun, ia tidak dapat mengakses panjangnya Dog, tetapi seharusnya tidak harus; sebagai gantinya, jika objek yang Stringseharusnya dibandingkan itu sendiri bukan aString, perbandingan harus menggunakan perilaku default (melaporkan bahwa objek lain tidak sama).

Waktu ketika downcasting harus dianggap sebagai yang paling meragukan adalah ketika objek yang dilemparkan "diketahui" memiliki tipe yang tepat. Secara umum, jika suatu objek dikenal sebagai Cat, seseorang harus menggunakan variabel tipe Cat, bukan variabel tipe Animal, untuk merujuknya. Namun ada kalanya ini tidak selalu berhasil. Sebagai contoh, Zookoleksi mungkin menahan pasangan objek dalam slot array genap / ganjil, dengan harapan bahwa objek di setiap pasangan akan dapat saling bertindak, bahkan jika mereka tidak dapat bertindak terhadap objek di pasangan lain. Dalam kasus seperti itu, objek dalam setiap pasangan masih harus menerima tipe parameter non-spesifik sehingga mereka, secara sintaksis , dapat melewati objek dari pasangan lain. Jadi, bahkan jika CatituplayWith(Animal other)Metode hanya akan bekerja ketika othersuatu Cat, Zooakan perlu untuk dapat melewatinya elemen dari Animal[], jadi tipe parameternya harus Animallebih daripada Cat.

Dalam kasus di mana downcasting secara sah tidak dapat dihindari, seseorang harus menggunakannya tanpa keraguan. Pertanyaan kuncinya adalah menentukan kapan seseorang secara bijaksana dapat menghindari downcasting, dan menghindarinya jika memungkinkan.

supercat
sumber
Dalam kasus string, Anda sebaiknya memiliki metode Object.equalToString(String string). Maka Anda memiliki boolean String.equal(Object object) { return object.equalStoString(this); }Jadi, tidak perlu downcast: Anda dapat menggunakan pengiriman dinamis.
Giorgio
@Iorgio: Pengiriman dinamis memiliki kegunaannya, tetapi umumnya lebih buruk daripada downcasting.
supercat
cara pengiriman dinamis umumnya lebih buruk daripada downcasting? Saya pikir itu sebaliknya
Bryan Chen
@BryanChen: Itu tergantung pada terminologi Anda. Saya tidak berpikir Objectmemiliki equalStoStringmetode virtual, dan saya akui saya tidak tahu bagaimana contoh yang dikutip akan bekerja di Jawa, tetapi dalam C #, pengiriman dinamis (berbeda dari pengiriman virtual) akan berarti bahwa kompiler pada dasarnya memiliki melakukan Refleksi berbasis nama pencarian pertama kali metode digunakan pada kelas, yang berbeda dari pengiriman virtual (yang hanya membuat panggilan melalui slot di tabel metode virtual yang diperlukan untuk berisi alamat metode yang valid).
supercat
Dari sudut pandang pemodelan, saya lebih suka pengiriman dinamis secara umum. Ini juga merupakan cara yang berorientasi objek untuk memilih prosedur berdasarkan jenis parameter inputnya.
Giorgio
1

Dalam implementasi metode abstrak yang menerima Hewan saya ingin menggunakan metode speak ().

Anda punya beberapa pilihan:

  • Gunakan refleksi untuk menelepon speakjika ada. Keuntungan: tidak ada ketergantungan Human. Kerugian: sekarang ada ketergantungan tersembunyi pada nama "berbicara".

  • Memperkenalkan antarmuka baru Speakerdan downcast ke antarmuka. Ini lebih fleksibel daripada tergantung pada jenis beton tertentu. Ini memiliki kelemahan yang harus Anda modifikasi Humanuntuk diterapkan Speaker. Ini tidak akan berfungsi jika Anda tidak dapat memodifikasiHuman

  • Downcast ke Human. Ini memiliki kelemahan yaitu Anda harus mengubah kode kapan pun Anda ingin subclass lain berbicara. Idealnya Anda ingin memperpanjang aplikasi dengan menambahkan kode tanpa berulang kali kembali dan mengubah kode lama.

kevin cline
sumber