Saya memiliki kelas dasar Base
,. Ini memiliki dua subclass, Sub1
dan Sub2
. Setiap subkelas memiliki beberapa metode tambahan. Misalnya, Sub1
memiliki Sandwich makeASandwich(Ingredients... ingredients)
, dan Sub2
memilikiboolean contactAliens(Frequency onFrequency)
.
Karena metode ini mengambil parameter yang berbeda dan melakukan hal yang sama sekali berbeda, mereka sepenuhnya tidak kompatibel, dan saya tidak bisa menggunakan polimorfisme untuk menyelesaikan masalah ini.
Base
menyediakan sebagian besar fungsionalitas, dan saya memiliki banyak koleksi Base
objek. Namun, semua Base
benda adalah a Sub1
atau a Sub2
, dan kadang-kadang saya perlu tahu yang mana.
Sepertinya ide yang buruk untuk melakukan hal berikut:
for (Base base : bases) {
if (base instanceof Sub1) {
((Sub1) base).makeASandwich(getRandomIngredients());
// ... etc.
} else { // must be Sub2
((Sub2) base).contactAliens(getFrequency());
// ... etc.
}
}
Jadi saya datang dengan strategi untuk menghindari ini tanpa casting. Base
sekarang memiliki metode ini:
boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();
Dan tentu saja, Sub1
menerapkan metode ini sebagai
boolean isSub1() { return true; }
Sub1 asSub1(); { return this; }
Sub2 asSub2(); { throw new IllegalStateException(); }
Dan Sub2
mengimplementasikannya dengan cara yang berlawanan.
Sayangnya, sekarang Sub1
dan Sub2
memiliki metode ini di API mereka sendiri. Jadi saya bisa melakukan ini, misalnya, pada Sub1
.
/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1(); { return this; }
/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2(); { throw new IllegalStateException(); }
Dengan cara ini, jika objek diketahui hanya a Base
, metode ini tidak ditinggalkan, dan dapat digunakan untuk "melemparkan" dirinya ke jenis yang berbeda sehingga saya dapat memanggil metode subclass di atasnya. Ini tampaknya elegan bagi saya, tetapi di sisi lain, saya agak menyalahgunakan anotasi usang sebagai cara untuk "menghapus" metode dari kelas.
Karena sebuah Sub1
instance benar - benar sebuah Base, masuk akal untuk menggunakan warisan daripada enkapsulasi. Apakah yang saya lakukan baik? Apakah ada cara yang lebih baik untuk menyelesaikan masalah ini?
sumber
instanceof
, dengan cara yang membutuhkan banyak pengetikan, rentan kesalahan, dan membuatnya sulit untuk menambahkan lebih banyak subkelas.Sub1
danSub2
tidak dapat digunakan secara bergantian, mengapa Anda memperlakukannya seperti itu? Mengapa tidak melacak 'pembuat sandwich' dan 'alien-contacters' Anda secara terpisah?Jawaban:
Tidak selalu masuk akal untuk menambahkan fungsi ke kelas dasar, seperti yang disarankan dalam beberapa jawaban lainnya. Menambahkan terlalu banyak fungsi kasus khusus dapat mengakibatkan pengikatan komponen yang tidak terkait.
Misalnya saya mungkin punya
Animal
kelas, denganCat
danDog
komponen. Jika saya ingin dapat mencetaknya, atau menunjukkannya di GUI, mungkin saya harus menambahrenderToGUI(...)
dan menambahsendToPrinter(...)
kelas dasar.Pendekatan yang Anda gunakan, menggunakan pemeriksaan tipe dan gips, rapuh - tetapi setidaknya tidak membuat keprihatinan dipisahkan.
Namun jika Anda mendapati diri Anda sering melakukan pemeriksaan semacam ini maka salah satu opsi adalah menerapkan pola pengunjung / pengiriman ganda untuknya. Ini terlihat seperti ini:
Sekarang kode Anda menjadi
Manfaat utama adalah bahwa jika Anda menambahkan subkelas, Anda akan mendapatkan kesalahan kompilasi daripada kasus yang hilang secara diam-diam. Kelas pengunjung baru juga menjadi target yang bagus untuk menarik fungsi. Misalnya mungkin masuk akal untuk pindah
getRandomIngredients()
keActOnBase
.Anda juga dapat mengekstrak logika perulangan: Misalnya fragmen di atas mungkin menjadi
Sedikit memijat lebih lanjut dan menggunakan lambdas dan streaming Java 8 akan membuat Anda bisa
IMO mana yang cukup rapi dan ringkas yang bisa Anda dapatkan.
Berikut adalah contoh Java 8 yang lebih lengkap:
sumber
Sub3
. Bukankah pengunjung memiliki masalah yang sama persisinstanceof
? Sekarang Anda perlu menambahonSub3
pengunjung dan itu rusak jika Anda lupa.onSub3
ke antarmuka pengunjung, Anda mendapatkan kesalahan kompilasi di setiap lokasi yang Anda buat Pengunjung baru yang belum diperbarui. Sebaliknya beralih pada jenis terbaik akan menghasilkan kesalahan run-time - yang bisa lebih sulit untuk dipicu.Dari sudut pandang saya: desain Anda salah .
Diterjemahkan ke bahasa alami, Anda mengatakan yang berikut:
Mengingat yang kita miliki
animals
, adacats
danfish
.animals
memiliki sifat, yang umum untukcats
danfish
. Tapi itu tidak cukup: ada beberapa sifat, yang membedakancat
darifish
, oleh karena itu Anda perlu subkelas.Sekarang Anda memiliki masalah, bahwa Anda lupa untuk memodelkan gerakan . Baik. Itu relatif mudah:
Tapi itu desain yang salah. Cara yang benar adalah:
Di mana
move
perilaku bersama akan diimplementasikan secara berbeda oleh masing-masing hewan.Ini berarti: desain Anda rusak.
Rekomendasi saya: refactor
Base
,Sub1
danSub2
.sumber
.move()
atau.attack()
dan benar abstrakthat
bagian - bukan memiliki.swim()
,.fly()
,.slide()
,.hop()
,.jump()
,.squirm()
,.phone_home()
, dll Bagaimana Anda refactor benar? Tidak dikenal tanpa contoh yang lebih baik ... tetapi umumnya masih merupakan jawaban yang benar - kecuali kode contoh dan detail lainnya menyarankan sebaliknya.move
vsfly
/run
/ etc, yang dapat dijelaskan dalam satu kalimat sebagai: Anda harus memberi tahu objek apa yang harus dilakukan, bukan bagaimana melakukannya. Dalam hal aksesor, seperti yang Anda sebutkan lebih seperti kasus nyata dalam komentar di sini, Anda harus mengajukan pertanyaan objek, bukan memeriksa kondisinya.Agak sulit untuk membayangkan keadaan di mana Anda memiliki sekelompok hal dan ingin mereka membuat sandwich atau menghubungi alien. Dalam kebanyakan kasus di mana Anda menemukan casting seperti itu Anda akan beroperasi dengan satu jenis - misalnya di dentang Anda menyaring satu set node untuk deklarasi di mana getAsFunction mengembalikan non-null, daripada melakukan sesuatu yang berbeda untuk setiap node dalam daftar.
Mungkin Anda perlu melakukan urutan tindakan dan sebenarnya tidak relevan bahwa objek yang melakukan tindakan terkait.
Jadi, alih-alih daftar
Base
, bekerja pada daftar tindakandimana
Anda dapat, jika sesuai, meminta Base menerapkan metode untuk mengembalikan aksi, atau apa pun cara lain yang Anda perlukan untuk memilih aksi dari instance dalam daftar base Anda (karena Anda mengatakan Anda tidak dapat menggunakan polimorfisme, jadi mungkin tindakan yang harus diambil bukan fungsi dari kelas tetapi beberapa properti lain dari basis, jika tidak, Anda hanya akan memberikan metode Base the act (Context))
sumber
Context
objek hanya akan memperburuk keadaan. Saya mungkin juga hanya meneruskanObject
metode, casting mereka, dan berharap mereka adalah tipe yang tepat.Context
tidak perlu menjadi kelas baru, atau bahkan sebuah antarmuka. Biasanya konteksnya hanya penelepon, jadi panggilan ada dalam formuliraction.act(this)
. JikagetFrequency()
dangetRandomIngredients()
merupakan metode statis, Anda bahkan mungkin tidak memerlukan konteks. Ini adalah bagaimana saya akan menerapkan katakan, antrian pekerjaan, yang masalah Anda terdengar sangat mirip.Bagaimana jika Anda memiliki subclass Anda mengimplementasikan satu atau lebih antarmuka yang menentukan apa yang bisa mereka lakukan? Sesuatu seperti ini:
Maka kelas Anda akan terlihat seperti ini:
Dan for-loop Anda akan menjadi:
Dua keuntungan utama dari pendekatan ini:
PS: Maaf untuk nama-nama antarmuka, saya tidak bisa memikirkan sesuatu yang lebih keren pada saat itu: D.
sumber
Pendekatan ini dapat menjadi salah satu yang baik dalam kasus di mana hampir semua jenis dalam sebuah keluarga akan baik langsung digunakan sebagai implementasi beberapa antarmuka yang memenuhi beberapa kriteria atau dapat digunakan untuk membuat sebuah implementasi dari antarmuka tersebut. Jenis-jenis koleksi bawaan IMHO akan mendapat manfaat dari pola ini, tetapi karena mereka tidak untuk tujuan contoh saya akan menciptakan antarmuka koleksi
BunchOfThings<T>
.Beberapa implementasi
BunchOfThings
bisa berubah; beberapa tidak. Dalam banyak kasus, sebuah objek yang mungkin ingin dimiliki Fred untuk menyimpan sesuatu yang dapat digunakan sebagaiBunchOfThings
dan tahu bahwa selain Fred akan dapat memodifikasinya. Persyaratan ini dapat dipenuhi dengan dua cara:Fred tahu bahwa itu memegang satu-satunya referensi untuk itu
BunchOfThings
, dan tidak ada referensi luar untuk ituBunchOfThings
ir internal yang ada di mana pun di alam semesta. Jika tidak ada orang lain yang memiliki referensi keBunchOfThings
atau internal, tidak ada orang lain akan dapat memodifikasinya, sehingga kendala akan terpenuhi.Baik
BunchOfThings
internal maupun internal yang tidak ada referensi luarnya, dapat dimodifikasi melalui cara apa pun. Jika benar-benar tidak ada yang dapat memodifikasiBunchOfThings
, maka batasannya akan terpenuhi.Salah satu cara untuk memenuhi kendala adalah dengan menyalin tanpa syarat objek apa pun yang diterima (secara rekursif memproses komponen bersarang). Lain akan menguji apakah objek yang diterima menjanjikan kekekalan dan, jika tidak, membuat salinannya, dan melakukan hal yang sama dengan komponen bersarang. Alternatif, yang cenderung lebih bersih daripada yang kedua dan lebih cepat dari yang pertama, adalah menawarkan
AsImmutable
metode yang meminta objek untuk membuat salinan dirinya yang tidak dapat diubah (menggunakanAsImmutable
komponen bersarang yang mendukungnya).Metode terkait juga dapat disediakan untuk
asDetached
(untuk digunakan ketika kode menerima objek dan tidak tahu apakah ia ingin mengubahnya, dalam hal ini objek yang dapat diubah harus diganti dengan objek yang dapat diubah baru, tetapi objek yang tidak dapat diubah dapat disimpan) as-is),asMutable
(dalam kasus-kasus di mana suatu objek tahu bahwa ia akan menyimpan objek yang sebelumnya kembali dariasDetached
, yaitu referensi yang tidak digunakan bersama untuk objek yang bisa berubah atau referensi yang dapat dibagi ke objek yang bisa berubah), danasNewMutable
(dalam kasus-kasus di mana kode menerima bagian luar referensi dan tahu itu akan ingin bermutasi salinan data di dalamnya - jika data yang masuk bisa berubah tidak ada alasan untuk memulai dengan membuat salinan yang tidak dapat diubah yang akan segera digunakan untuk membuat salinan yang dapat diubah dan kemudian ditinggalkan).Perhatikan bahwa walaupun
asXX
metode dapat mengembalikan tipe yang sedikit berbeda, peran sebenarnya adalah untuk memastikan bahwa objek yang dikembalikan akan memenuhi kebutuhan program.sumber
Mengabaikan masalah apakah Anda memiliki desain yang bagus atau tidak, dan seandainya itu bagus atau setidaknya dapat diterima, saya ingin mempertimbangkan kemampuan subclass, bukan tipe.
Karena itu, baik:
Pindahkan pengetahuan tentang keberadaan sandwich dan alien ke kelas dasar, meskipun Anda tahu beberapa contoh kelas dasar tidak bisa melakukannya. Terapkan di kelas dasar untuk melempar pengecualian, dan ubah kode menjadi:
Kemudian satu atau kedua subclass menimpa
canMakeASandwich()
, dan hanya satu yang mengimplementasikan masing-masingmakeASandwich()
dancontactAliens()
.Gunakan antarmuka, bukan subkelas konkret, untuk mendeteksi kemampuan apa yang dimiliki suatu jenis. Biarkan kelas dasar saja, dan ubah kode ke:
atau mungkin (dan merasa bebas untuk mengabaikan opsi ini jika tidak sesuai dengan gaya Anda, atau dalam hal apa pun gaya Java yang Anda anggap masuk akal):
Secara pribadi saya biasanya tidak suka gaya terakhir menangkap pengecualian yang diharapkan, karena risiko menangkap secara tidak tepat
ClassCastException
keluar darigetRandomIngredients
ataumakeASandwich
, tapi YMMV.sumber
<
adalah untuk menghindari penangkapanArrayIndexOutOfBoundsException
, dan beberapa orang memutuskan untuk melakukannya juga. Tidak ada perhitungan untuk rasa ;-) Pada dasarnya "masalah" saya di sini adalah saya bekerja di Python, di mana gaya yang disukai biasanya adalah bahwa menangkap pengecualian apa pun lebih baik daripada menguji terlebih dahulu apakah pengecualian akan terjadi, tidak peduli betapa sederhananya tes lanjutan akan . Mungkin saja kemungkinan tidak layak disebutkan di Jawa.Di sini kita memiliki kasus menarik tentang kelas dasar yang menurunkan dirinya ke kelas turunannya sendiri. Kami tahu betul bahwa ini biasanya buruk, tetapi jika kami ingin mengatakan bahwa kami menemukan alasan yang bagus, mari kita lihat dan lihat apa kendala yang mungkin terjadi:
Jika 4 maka kita memiliki: 5. Kebijakan kelas turunan selalu di bawah kendali politik yang sama dengan kebijakan kelas dasar.
2 dan 5 keduanya secara langsung menyiratkan bahwa kita dapat menghitung semua kelas turunan, yang berarti bahwa tidak ada kelas turunan luar.
Tapi ini masalahnya. Jika semuanya milik Anda, Anda dapat mengganti if dengan abstraksi yang merupakan panggilan metode virtual (bahkan jika itu omong kosong) dan menyingkirkan if dan self-cast. Karena itu, jangan lakukan itu. Desain yang lebih baik tersedia.
sumber
Letakkan metode abstrak di kelas dasar yang melakukan satu hal di Sub1 dan yang lainnya di Sub2, dan memanggil metode abstrak itu di loop campuran Anda.
Perhatikan bahwa ini mungkin memerlukan perubahan lain tergantung pada bagaimana getRandomIngredients dan getFrequency didefinisikan. Tapi, jujur saja, di dalam kelas yang menghubungi alien mungkin adalah tempat yang lebih baik untuk mendefinisikan getFrequency.
Kebetulan, metode asSub1 dan asSub2 Anda jelas praktik yang buruk. Jika Anda akan melakukan ini, lakukan dengan casting. Tidak ada metode ini memberi Anda bahwa casting tidak.
sumber
Anda bisa mendorong keduanya
makeASandwich
dancontactAliens
ke kelas dasar dan kemudian mengimplementasikannya dengan implementasi dummy. Karena tipe pengembalian / argumen tidak dapat diselesaikan ke kelas dasar umum.Ada kerugian yang jelas untuk hal semacam ini, seperti apa artinya untuk kontrak metode dan apa yang Anda dapat dan tidak dapat menyimpulkan tentang konteks hasilnya. Anda tidak dapat berpikir karena saya gagal membuat sandwich bahan yang digunakan dalam upaya dll
sumber
Apa yang Anda lakukan benar-benar sah. Jangan memperhatikan para penentang yang hanya mengulangi dogma karena mereka membacanya di beberapa buku. Dogma tidak punya tempat di bidang teknik.
Saya telah menggunakan mekanisme yang sama beberapa kali, dan saya dapat mengatakan dengan keyakinan bahwa java runtime juga dapat melakukan hal yang sama di setidaknya satu tempat yang dapat saya pikirkan, sehingga meningkatkan kinerja, kegunaan, dan keterbacaan kode yang menggunakannya.
Ambil contoh
java.lang.reflect.Member
, yang merupakan dasar darijava.lang.reflect.Field
danjava.lang.reflect.Method
. (Hirarki sebenarnya sedikit lebih rumit dari itu, tapi itu tidak relevan.) Bidang dan metode adalah hewan yang sangat berbeda: satu memiliki nilai yang dapat Anda peroleh atau tetapkan, sementara yang lain tidak memiliki hal semacam itu, tetapi dapat digunakan dengan sejumlah parameter dan dapat mengembalikan nilai. Jadi, bidang dan metode sama-sama anggota, tetapi hal-hal yang dapat Anda lakukan dengannya berbeda satu sama lain seperti membuat sandwich vs menghubungi alien.Sekarang, ketika menulis kode yang menggunakan refleksi kita sering memiliki
Member
di tangan kita, dan kita tahu bahwa itu adalah aMethod
atau aField
, (atau, jarang, sesuatu yang lain,) namun kita harus melakukan semua yang membosankaninstanceof
untuk mencari tahu dengan tepat apa itu dan kemudian kita harus membuangnya untuk mendapatkan referensi yang tepat untuk itu. (Dan ini tidak hanya membosankan, tetapi juga tidak berkinerja sangat baik.)Method
Kelas bisa dengan mudah menerapkan pola yang Anda gambarkan, sehingga membuat kehidupan ribuan programmer lebih mudah.Tentu saja, teknik ini hanya dapat dijalankan dalam hierarki kecil, yang didefinisikan dengan baik dari kelas yang digabungkan erat yang Anda miliki (dan akan selalu memiliki) kontrol tingkat sumber: Anda tidak ingin melakukan hal seperti itu jika hierarki kelas Anda adalah dapat diperpanjang oleh orang-orang yang tidak bebas untuk memperbaiki kelas dasar.
Berikut adalah bagaimana apa yang telah saya lakukan berbeda dari apa yang telah Anda lakukan:
Kelas dasar menyediakan implementasi default untuk seluruh
asDerivedClass()
keluarga metode, masing-masing kembalinull
.Setiap kelas turunan hanya menimpa salah satu
asDerivedClass()
metode, mengembalikanthis
bukannull
. Itu tidak mengesampingkan sisanya, juga tidak ingin tahu apa-apa tentang mereka. Jadi, tidak adaIllegalStateException
yang terlempar.Kelas dasar juga menyediakan
final
implementasi untuk seluruhisDerivedClass()
keluarga metode, diberi kode sebagai berikut:return asDerivedClass() != null;
Dengan cara ini, jumlah metode yang perlu diganti oleh kelas turunan diminimalkan.Saya belum menggunakan
@Deprecated
mekanisme ini karena saya tidak memikirkannya. Sekarang Anda memberi saya ide, saya akan menggunakannya, terima kasih!C # memiliki mekanisme terkait bawaan melalui penggunaan
as
kata kunci. Dalam C # Anda bisa mengatakanDerivedClass derivedInstance = baseInstance as DerivedClass
dan Anda akan mendapatkan referensi keDerivedClass
jika AndabaseInstance
dari kelas itu, ataunull
jika tidak. Ini (secara teoritis) berkinerja lebih baik daripadais
diikuti oleh para pemeran, (is
adalah kata kunci C # yang diakui lebih baik untukinstanceof
,) tetapi mekanisme kustom yang kami hasilkan dengan kerajinan tangan bahkan lebih baik: pasanganinstanceof
operasi -dan-pemeran Jawa, juga sebagaias
operator C # tidak melakukan secepat panggilan metode virtual tunggal pendekatan kustom kami.Saya dengan ini mengemukakan proposisi bahwa teknik ini harus dinyatakan sebagai pola dan bahwa nama yang bagus harus ditemukan untuk itu.
Wah, terima kasih untuk downvotes!
Ringkasan kontroversi, untuk menyelamatkan Anda dari kesulitan membaca komentar:
Orang-orang keberatan karena desain aslinya salah, artinya Anda tidak boleh memiliki kelas yang sangat berbeda yang berasal dari kelas dasar yang sama, atau bahkan jika Anda melakukannya, kode yang menggunakan hierarki seperti itu seharusnya tidak boleh berada dalam posisi memiliki referensi dasar dan perlu mencari tahu kelas turunannya. Karena itu, kata mereka, mekanisme self-casting yang diajukan oleh pertanyaan ini dan oleh jawaban saya, yang meningkatkan penggunaan desain asli, seharusnya tidak pernah diperlukan sejak awal. (Mereka tidak benar-benar mengatakan apa-apa tentang mekanisme self-casting itu sendiri, mereka hanya mengeluh tentang sifat desain yang dimaksudkan untuk diterapkan mekanisme itu.)
Namun, pada contoh di atas saya miliki sudah menunjukkan bahwa pencipta runtime java itu sebenarnya memilih desain tepatnya tersebut untuk
java.lang.reflect.Member
,Field
,Method
hirarki, dan di komentar di bawah ini saya juga menunjukkan bahwa pencipta C # runtime secara independen tiba di setara desain untukSystem.Reflection.MemberInfo
,FieldInfo
,MethodInfo
hirarki. Jadi, ini adalah dua skenario dunia nyata yang berbeda yang duduk tepat di bawah hidung semua orang dan yang memiliki solusi yang bisa diterapkan dengan menggunakan desain yang tepat seperti itu.Itulah semua komentar berikut. Mekanisme self-casting hampir tidak disebutkan.
sumber
java.lang.reflection.Member
hierarki. Para pencipta runtime java masuk dalam situasi seperti ini, saya tidak berpikir Anda akan mengatakan mereka merancang diri mereka sendiri menjadi sebuah kotak, atau menyalahkan mereka karena tidak mengikuti semacam praktik terbaik? Jadi, maksud saya di sini adalah bahwa sekali Anda memiliki situasi seperti ini, mekanisme ini adalah cara yang baik untuk memperbaiki keadaan.Member
antarmuka, tetapi hanya berfungsi untuk memeriksa kualifikasi akses. Biasanya, Anda mendapatkan daftar metode atau properti dan menggunakannya secara langsung (tanpa mencampurkannya, jadi tidakList<Member>
muncul), sehingga Anda tidak perlu melakukan cast. Catatan yangClass
tidak memberikangetMembers()
metode. Tentu saja, seorang programmer yang tidak mengerti mungkin menciptakannyaList<Member>
, tetapi itu akan membuat sedikit masuk akal jika membuatnyaList<Object>
dan menambahkan beberapaInteger
atauString
ke dalamnya.null
bukannya pengecualian tidak membuatnya jauh lebih baik. Semua sama sederajat, pengunjung lebih aman saat melakukan pekerjaan yang sama.