Apakah saya tetap bisa melanggar LSP?

10

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, Sworddan Reloadable. Jika Reloadableberisi 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, Reloadablesenjata, jelas dapat memuat ulang, tetapi Swordtidak 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 Swordagar 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
    }
}

sumber
2
Anda bisa melakukannya class Weapon { bool supportsReload(); void reload(); }. Klien akan menguji jika didukung sebelum memuat ulang. reloaddidefinisikan secara kontraktual untuk melempar iff !supportsReload(). Itu mematuhi LSP iff drived kelas mematuhi protokol yang baru saja saya uraikan.
usr
3
Apakah Anda reload()mengosongkan atau standardActionstidak 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.
usr
27
Saya menulis serangkaian artikel yang mengeksplorasi berbagai masalah dengan berbagai teknik untuk menyelesaikan masalah ini. Kesimpulannya: jangan mencoba untuk menangkap aturan gim Anda dalam sistem jenis bahasa . Menangkap aturan gim dalam objek yang mewakili dan menegakkan aturan di level logika gim, bukan level sistem tipe . Tidak ada alasan untuk percaya bahwa sistem tipe apa pun yang Anda gunakan cukup canggih untuk mewakili logika permainan Anda. ericlippert.com/2015/04/27/wizards-and-warriors-part-one
Eric Lippert
2
@EricLippert - Terima kasih atas tautan Anda. Saya telah menemukan blog ini berkali-kali, tetapi beberapa poin membuat saya tidak begitu mengerti, tapi itu bukan kesalahan Anda. Saya belajar OOP sendiri dan menemukan kepala sekolah SOLID. Pertama kali saya menemukan blog Anda, saya tidak memahaminya sama sekali, tetapi saya belajar sedikit lebih banyak dan membaca blog Anda lagi, dan perlahan-lahan mulai memahami bagian dari apa yang dikatakan. Suatu hari, saya akan sepenuhnya memahami segala sesuatu dalam seri itu. Saya harap: D
6
@ SR "jika tidak melakukan apa-apa atau melempar pengecualian, Anda melanggar" - Saya pikir Anda salah membaca pesan dari artikel itu. Masalahnya bukan secara langsung bahwa setAltitude tidak melakukan apa-apa, tetapi gagal memenuhi postcondition "burung akan ditarik pada ketinggian yang ditetapkan". Jika Anda mendefinisikan postcondition "reload" sebagai "jika amunisi yang cukup tersedia, senjata dapat menyerang lagi", maka tidak melakukan apa-apa adalah implementasi yang valid untuk senjata yang tidak menggunakan amunisi.
Sebastian Redl

Jawaban:

16

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

  • Warisan yang digunakan untuk memodelkan tipe jumlah / gabungan, mis: a Baseadalah salah satu CaseAatau CaseB. 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 membuat CaseC, maka kode dengan asumsi bahwa Basehanya bisa a CaseAatau CaseBsalah. Scala dapat melakukan ini dengan aman dengan case classkonsepnya. Di Jawa, ini dapat dimodelkan ketika Basekelas 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 Weaponkelas dasar dari mana Gundan Swordmewarisi" 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 juga reload()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 Gundan Swordmungkin sama sekali tidak terkait. Sedangkan Gunkaleng fire()dan reload()satu Sword-satunya mungkin strike(). 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 memiliki Attackstrategi yang Anda berikan myGun::fireatau () -> 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 OnDamageReceivedatau Attack. Dengan senjata Anda, kita mungkin memiliki MeleeAttack, RangedAttack, dan Reloadslot. Slot ini mungkin kosong, dalam hal ini objek tidak menyediakan kemampuan ini. Slot kemudian disebut kondisional: if (item.attack != null) item.attack.perform().

amon
sumber
Semacam seperti SP dengan cara. Mengapa slot harus dikosongkan? Jika kamus tidak mengandung tindakan, cukup lakukan apa saja
@ SR Apakah slot kosong atau tidak ada tidak masalah, dan tergantung pada mekanisme yang digunakan untuk mengimplementasikan slot ini. Saya menulis jawaban ini dengan asumsi bahasa yang cukup statis di mana slot adalah bidang contoh dan selalu ada (yaitu desain kelas normal di Jawa). Jika memilih model yang lebih dinamis di mana slot adalah entri dalam kamus (seperti menggunakan HashMap di Jawa, atau objek Python normal), maka slot tidak harus ada. Perhatikan bahwa pendekatan yang lebih dinamis memberikan banyak jenis keamanan, yang biasanya tidak diinginkan.
amon
Saya setuju bahwa objek dunia nyata tidak dapat dimodelkan dengan baik. Jika saya memahami posting Anda, pepatah Anda dapat menggunakan Pola Strategi?
2
@ SR Ya, Pola Strategi dalam beberapa bentuk kemungkinan merupakan pendekatan yang masuk akal. Bandingkan juga Pola Objek Jenis yang terkait: gameprogrammingpatterns.com/type-object.html
amon
3

Karena memiliki strategi attacktidak 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 swordmewarisi dari weaponyang 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.

Telastyn
sumber
Saya pikir ini sempurna. Saya bisa menggunakan SP, tapi itu trade off, hanya harus mewaspadai mereka. Lihat hasil edit saya, untuk apa yang ada dalam pikiran saya.
1
Fwiw: pedang memiliki amunisi tak terbatas: Anda dapat terus menggunakannya tanpa membaca selamanya; memuat ulang tidak melakukan apa-apa karena Anda memiliki penggunaan yang tak terbatas untuk memulai; kisaran satu / jarak dekat: itu adalah senjata jarak dekat. Bukan tidak mungkin untuk memikirkan semua statistik / tindakan dengan cara yang bekerja untuk jarak dekat dan jarak jauh. Namun, seiring bertambahnya usia, saya menggunakan semakin sedikit warisan yang mendukung antarmuka, kompetisi, dan apa pun namanya untuk menggunakan satu Weaponkelas dengan instance pedang dan senapan.
CAD97
Pedang di Destiny 2 pedang menggunakan amunisi untuk beberapa alasan!
@ CAD97 - Ini adalah jenis pemikiran yang saya lihat mengenai masalah ini. Memiliki pedang dengan amunisi yang tak terbatas, jadi tidak perlu memuat ulang. Ini hanya mendorong masalah di sekitar atau menyembunyikannya. Bagaimana jika saya memperkenalkan granat, lalu bagaimana? Granat tidak memiliki amunisi atau menembak, dan seharusnya tidak menyadari metode seperti itu.
1
Saya dengan CAD97 tentang ini. Dan akan menciptakan WeaponBuilderyang bisa membangun pedang dan senjata dengan menyusun senjata strategi.
Chris Wohlert
3

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:

if (isEquipped(weapon)) {
   reload();
}

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,

if (canReload(weapon)) {
   reload();
}
else if (canSharpen(weapon)) {
  sharpen();
}
else if (canPollish(weapon)) {
  polish();
}

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.

Batavia
sumber
Katakanlah saya memiliki kamus senjata internal yang berisi aksi untuk senjata, dan ketika pengguna memasukkan "Reload" itu memeriksa kamus, ex, weaponActions.containsKey (action) jika demikian, ambil objek yang terkait dengannya, dan lakukan Itu. Alih-alih kelas senjata dengan beberapa pernyataan if
Lihat edit di atas. Ini adalah apa yang ada dalam pikiran saya ketika menggunakan SP
0

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.

gnasher729
sumber
0

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

Ewan
sumber
Saya pikir SP dapat menangani semua itu (lihat edit di atas), pistol akan SafteyOn()dan Swordakan memiliki wipeOffBlood(). Setiap senjata tidak mengetahui metode lain (dan seharusnya tidak)
SP baik-baik saja, tetapi setara dengan downcasting tanpa keamanan jenis. Saya kira saya agak menjawab pertanyaan yang berbeda, izinkan saya memperbarui
Ewan
2
Dengan sendirinya pola strategi tidak menyiratkan pencarian dinamis dari strategi dalam daftar atau kamus. Yaitu keduanya weapon.do("attack")dan tipe-aman weapon.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.
amon
yang tidak akan berfungsi dalam situasi ini karena ada dua serangan tindakan terpisah dan memuat ulang, yang perlu Anda ikat ke beberapa input pengguna
Ewan