Saya menindaklanjuti pertanyaan ini , tetapi saya mengalihkan fokus saya dari kode ke prinsip.
Dari pemahaman saya tentang prinsip substitusi Liskov (LSP), metode apa pun yang ada di kelas dasar saya, mereka harus diimplementasikan dalam subkelas saya, dan menurut halaman ini , jika Anda mengganti metode di kelas dasar dan tidak melakukan apa pun atau melempar pengecualian, Anda melanggar prinsip.
Sekarang, masalah saya dapat diringkas seperti ini: Saya memiliki abstrak Weapon
class
, dan dua kelas, Sword
dan Reloadable
. Jika Reloadable
berisi spesifik method
, dipanggil Reload()
, saya harus downcast untuk mengaksesnya method
, dan, idealnya, Anda ingin menghindarinya.
Saya kemudian berpikir untuk menggunakan Strategy Pattern
. Dengan cara ini setiap senjata hanya menyadari tindakan yang mampu dilakukannya, jadi misalnya, Reloadable
senjata, jelas dapat memuat ulang, tetapi Sword
tidak bisa, dan bahkan tidak menyadari adanya Reload class/method
. Seperti yang saya nyatakan dalam posting Stack Overflow saya, saya tidak perlu downcast, dan saya bisa mengelola List<Weapon>
koleksi.
Di forum lain , jawaban pertama disarankan Sword
agar disadari Reload
, tapi jangan lakukan apa-apa. Jawaban yang sama diberikan pada halaman Stack Overflow yang saya tautkan di atas.
Saya tidak sepenuhnya mengerti mengapa. Mengapa melanggar prinsip dan membiarkan Pedang sadar Reload
, dan membiarkannya kosong? Seperti yang saya katakan di posting Stack Overflow saya, SP, cukup banyak memecahkan masalah saya.
Mengapa ini bukan solusi yang layak?
public final Weapon{
private final String name;
private final int damage;
private final List<AttackStrategy> validactions;
private final List<Actions> standardActions;
private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
{
this.name = name;
this.damage = damage;
standardActions = new ArrayList<Actions>(standardActions);
validAttacks = new ArrayList<AttackStrategy>(validActions);
}
public void standardAction(String action){} // -- Can call reload or aim here.
public int attack(String action){} // - Call any actions that are attacks.
public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
return new Weapon(name, damage,standardActions, attacks) ;
}
}
Antarmuka dan Implementasi Serangan:
public interface AttackStrategy{
void attack(Enemy enemy);
}
public class Shoot implements AttackStrategy {
public void attack(Enemy enemy){
//code to shoot
}
}
public class Strike implements AttackStrategy {
public void attack(Enemy enemy){
//code to strike
}
}
class Weapon { bool supportsReload(); void reload(); }
. Klien akan menguji jika didukung sebelum memuat ulang.reload
didefinisikan secara kontraktual untuk melempar iff!supportsReload()
. Itu mematuhi LSP iff drived kelas mematuhi protokol yang baru saja saya uraikan.reload()
mengosongkan ataustandardActions
tidak mengandung tindakan reload hanya mekanisme yang berbeda. Tidak ada perbedaan mendasar. Anda bisa melakukan keduanya. => Solusi Anda adalah layak (yang pertanyaan Anda) .; Pedang tidak perlu tahu tentang memuat ulang jika Senjata berisi implementasi default kosong.Jawaban:
LSP prihatin dengan subtipe dan polimorfisme. Tidak semua kode benar-benar menggunakan fitur ini, dalam hal ini LSP tidak relevan. Dua kasus penggunaan umum konstruksi bahasa pewarisan yang bukan merupakan kasus subtipe adalah:
Warisan digunakan untuk mewarisi implementasi kelas dasar, tetapi tidak antarmuka-nya. Dalam hampir semua kasus, komposisi harus lebih disukai. Bahasa seperti Java tidak dapat memisahkan warisan implementasi dan antarmuka, tetapi misalnya C ++ memiliki
private
warisan.Warisan yang digunakan untuk memodelkan tipe jumlah / gabungan, mis: a
Base
adalah salah satuCaseA
atauCaseB
. Tipe dasar tidak menyatakan antarmuka yang relevan. Untuk menggunakan instansnya, Anda harus melemparkannya ke jenis beton yang benar. Pengecoran bisa dilakukan dengan aman dan bukan masalahnya. Sayangnya, banyak bahasa OOP tidak dapat membatasi subtipe kelas dasar hanya subtipe yang dimaksud. Jika kode eksternal dapat membuatCaseC
, maka kode dengan asumsi bahwaBase
hanya bisa aCaseA
atauCaseB
salah. Scala dapat melakukan ini dengan aman dengancase class
konsepnya. Di Jawa, ini dapat dimodelkan ketikaBase
kelas abstrak dengan konstruktor pribadi, dan kelas statis bersarang kemudian mewarisi dari basis.Beberapa konsep seperti hirarki konseptual objek dunia nyata memetakan dengan sangat buruk ke dalam model berorientasi objek. Pikiran seperti "Pistol adalah senjata, dan pedang adalah senjata, oleh karena itu saya akan memiliki
Weapon
kelas dasar dari manaGun
danSword
mewarisi" menyesatkan: kata sebenarnya adalah-hubungan tidak menyiratkan hubungan seperti itu dalam model kami. Salah satu masalah terkait adalah bahwa objek dapat menjadi milik beberapa hierarki konseptual atau dapat mengubah afiliasi hierarki mereka selama waktu berjalan, yang sebagian besar bahasa tidak dapat memodelkan karena warisan biasanya per-kelas bukan per-objek, dan didefinisikan pada waktu desain bukan waktu-lari.Saat mendesain model OOP kita tidak boleh berpikir tentang hierarki, atau bagaimana satu kelas “meluas” yang lain. Kelas dasar bukan tempat untuk memfaktorkan bagian umum dari beberapa kelas. Sebagai gantinya, pikirkan tentang bagaimana objek Anda akan digunakan, yaitu perilaku seperti apa yang dibutuhkan oleh pengguna objek ini.
Di sini, pengguna mungkin perlu
attack()
dengan senjata dan mungkin jugareload()
mereka. Jika kita ingin membuat hierarki tipe, maka kedua metode ini harus dalam tipe dasar, meskipun senjata yang tidak dapat dimuat ulang dapat mengabaikan metode itu dan tidak melakukan apa pun saat dipanggil. Jadi kelas dasar tidak mengandung bagian-bagian umum, tetapi antarmuka gabungan dari semua subclass. Subkelas tidak berbeda dalam antarmuka mereka, tetapi hanya dalam implementasi antarmuka ini.Tidak perlu membuat hierarki. Kedua jenis
Gun
danSword
mungkin sama sekali tidak terkait. SedangkanGun
kalengfire()
danreload()
satuSword
-satunya mungkinstrike()
. Jika Anda perlu mengelola objek-objek ini secara polimorfis, Anda dapat menggunakan Pola Adaptor untuk menangkap aspek yang relevan. Di Java 8 ini dimungkinkan dengan antarmuka fungsional dan lambdas / metode referensi. Misalnya Anda mungkin memilikiAttack
strategi yang Anda berikanmyGun::fire
atau() -> mySword.strike()
.Akhirnya, kadang-kadang masuk akal untuk menghindari subclass sama sekali, tetapi model semua objek melalui satu tipe. Ini sangat relevan dalam gim karena banyak objek gim yang tidak cocok dengan hierarki apa pun, dan mungkin memiliki banyak kemampuan berbeda. Misalnya, permainan peran dapat memiliki item yang merupakan item pencarian, menambah statistik Anda dengan kekuatan +2 saat dilengkapi, memiliki peluang 20% untuk mengabaikan kerusakan yang diterima, dan memberikan serangan jarak dekat. Atau mungkin pedang yang bisa diisi ulang karena * sihir *. Siapa yang tahu apa yang dibutuhkan cerita itu.
Daripada mencoba mencari hierarki kelas untuk kekacauan itu, lebih baik memiliki kelas yang menyediakan slot untuk berbagai kemampuan. Slot ini dapat diubah saat runtime. Setiap slot akan menjadi strategi / panggilan balik seperti
OnDamageReceived
atauAttack
. Dengan senjata Anda, kita mungkin memilikiMeleeAttack
,RangedAttack
, danReload
slot. Slot ini mungkin kosong, dalam hal ini objek tidak menyediakan kemampuan ini. Slot kemudian disebut kondisional:if (item.attack != null) item.attack.perform()
.sumber
Karena memiliki strategi
attack
tidak cukup untuk kebutuhan Anda. Tentu, ini memungkinkan Anda untuk mengabstraksi tindakan apa yang dapat dilakukan item tersebut, tetapi apa yang terjadi ketika Anda perlu mengetahui rentang senjata? Atau kapasitas amunisi? Atau amunisi macam apa yang dibutuhkan? Anda kembali ke downcasting untuk mendapatkan itu. Dan memiliki tingkat fleksibilitas seperti itu akan membuat UI sedikit lebih sulit untuk diimplementasikan, karena itu perlu memiliki pola strategi yang sama untuk menangani semua kemampuan.Semua yang dikatakan, saya tidak terlalu setuju dengan jawaban untuk pertanyaan Anda yang lain. Memiliki
sword
mewarisi dariweapon
yang mengerikan, OO naif yang selalu mengarah ke metode no-op atau jenis-cek berserakan kode.Tetapi pada akar masalah, tidak ada solusi yang salah . Anda dapat menggunakan kedua solusi untuk membuat game yang berfungsi yang menyenangkan untuk dimainkan. Masing-masing datang dengan trade-off mereka sendiri, sama seperti solusi apa pun yang Anda pilih.
sumber
Weapon
kelas dengan instance pedang dan senapan.WeaponBuilder
yang bisa membangun pedang dan senjata dengan menyusun senjata strategi.Tentu saja itu solusi yang layak; itu hanya ide yang sangat buruk.
Masalahnya bukan jika Anda memiliki instance tunggal ini di mana Anda meletakkan ulang pada kelas dasar Anda. Masalahnya adalah Anda juga harus meletakkan "swing", "shoot" "parry", "knock", "polish", "disassemble", "sharpen", dan "ganti paku ujung runcing klub" metode pada kelas dasar Anda.
Inti dari LSP adalah bahwa algoritma tingkat atas Anda perlu bekerja dan masuk akal. Jadi jika saya memiliki kode seperti ini:
Sekarang jika itu melempar pengecualian yang tidak diimplementasikan dan membuat program Anda macet maka itu ide yang sangat buruk.
Jika kode Anda terlihat seperti ini,
maka kode Anda dapat menjadi berantakan dengan properti yang sangat spesifik yang tidak ada hubungannya dengan ide 'senjata' abstrak.
Namun, jika Anda menerapkan penembak orang pertama dan semua senjata Anda dapat menembak / memuat kembali kecuali satu pisau maka (dalam konteks spesifik Anda), sangat masuk akal untuk meminta reload pisau Anda tidak melakukan apa-apa karena itu adalah pengecualian dan kemungkinan memiliki kelas dasar Anda berantakan dengan properti spesifik rendah.
Pembaruan: Cobalah untuk memikirkan tentang abstrak case / terms. Misalnya, mungkin setiap senjata memiliki aksi "persiapan" yang merupakan pemuatan ulang untuk senjata dan menghunus pedang.
sumber
Jelas itu OK jika Anda tidak membuat subclass dengan maksud mengganti instance kelas dasar, tetapi jika Anda membuat subclass menggunakan kelas dasar sebagai repositori fungsionalitas yang nyaman.
Sekarang apakah itu ide yang bagus atau tidak, masih bisa diperdebatkan, tetapi jika Anda tidak pernah mengganti subkelas dengan baseclass, maka fakta bahwa itu tidak berhasil bukanlah masalah. Anda mungkin memiliki masalah, tetapi LSP bukan masalah dalam kasus ini.
sumber
LSP baik karena memungkinkan kode panggilan untuk tidak khawatir tentang cara kerja kelas.
misalnya. Saya dapat memanggil Weapon.Attack () pada semua Senjata yang dipasang di BattleMech saya dan tidak khawatir bahwa beberapa dari mereka akan melempar pengecualian dan membuat crash game saya.
Sekarang dalam kasus Anda, Anda ingin memperluas jenis dasar Anda dengan fungsionalitas baru. Attack () bukan masalah, karena kelas Gun dapat melacak amunisinya dan berhenti menembak ketika habis. Tapi Reload () adalah sesuatu yang baru dan bukan bagian dari menjadi senjata.
Solusi mudahnya adalah dengan downcast, saya tidak berpikir Anda perlu khawatir tentang kinerja yang terlalu, Anda tidak akan melakukannya setiap frame.
Atau Anda dapat menilai kembali arsitektur Anda dan mempertimbangkan bahwa dalam abstrak semua Senjata dapat dimuat kembali, dan beberapa senjata tidak perlu dimuat ulang.
Maka Anda tidak memperpanjang kelas untuk senjata lagi, atau melanggar LSP.
Tapi itu bermasalah jangka panjang karena Anda terikat untuk memikirkan lebih banyak kasus khusus, Gun.SafteyOn (), Sword.WipeOffBlood () dll dan jika Anda memasukkan semuanya ke dalam Senjata, maka Anda memiliki kelas dasar umum super rumit yang Anda simpan harus berubah.
sunting: mengapa pola strateginya Buruk (tm)
Bukan, tetapi pertimbangkan pengaturan, kinerja, dan kode keseluruhan.
Saya harus memiliki konfigurasi di suatu tempat yang memberitahu saya bahwa pistol dapat memuat ulang. Ketika saya instantiate senjata, saya harus membaca konfigurasi itu dan secara dinamis menambahkan semua metode, periksa tidak ada nama duplikat dll
Ketika saya memanggil metode saya harus mengulang daftar tindakan dan melakukan pencocokan string untuk melihat mana yang harus dipanggil.
Ketika saya mengkompilasi kode dan memanggil Weapon.Do ("atack") alih-alih "menyerang" saya tidak akan mendapatkan kesalahan pada kompilasi.
Ini bisa menjadi solusi yang cocok untuk beberapa masalah, misalkan Anda memiliki ratusan senjata semuanya dengan kombinasi metode acak yang berbeda, tetapi Anda kehilangan banyak manfaat OO dan pengetikan yang kuat. Itu tidak benar-benar menyelamatkan Anda apa pun dari downcasting
sumber
SafteyOn()
danSword
akan memilikiwipeOffBlood()
. Setiap senjata tidak mengetahui metode lain (dan seharusnya tidak)weapon.do("attack")
dan tipe-amanweapon.attack.perform()
dapat menjadi contoh dari pola strategi. Mencari strategi berdasarkan nama hanya diperlukan ketika mengkonfigurasi objek dari file konfigurasi, meskipun menggunakan refleksi akan sama-sama aman.