Apa yang bisa salah jika prinsip substitusi Liskov dilanggar?

27

Saya mengikuti pertanyaan yang sangat masuk akal ini tentang kemungkinan pelanggaran prinsip Pergantian Liskov. Saya tahu apa prinsip Substitusi Liskov, tetapi yang masih belum jelas dalam benak saya adalah apa yang salah jika saya sebagai pengembang tidak memikirkan prinsip tersebut saat menulis kode berorientasi objek.

Kutu buku
sumber
6
Apa yang salah jika Anda tidak mengikuti LSP? Skenario terburuk: Anda akhirnya memanggil Code-thulhu! ;)
FrustratedWithFormsDesigner
1
Sebagai penulis pertanyaan orisinal itu, saya harus menambahkan bahwa itu adalah pertanyaan akademis. Meskipun pelanggaran dapat menyebabkan kesalahan dalam kode, saya tidak pernah memiliki masalah serius bug atau pemeliharaan yang bisa saya lakukan untuk pelanggaran LSP.
Paul T Davies
2
@Paul Jadi, Anda tidak pernah memiliki masalah dengan program Anda karena hierarki OO yang berbelit-belit (yang tidak Anda rancang sendiri, tetapi mungkin harus diperpanjang) di mana kontrak diputus kiri dan kanan oleh orang-orang yang tidak yakin tentang tujuan kelas dasar memulai dengan? Saya iri padamu! :)
Andres F.
@ PaulTMelakukan keparahan konsekuensi tergantung pada apakah pengguna (programmer yang menggunakan perpustakaan) memiliki pengetahuan rinci tentang implementasi perpustakaan (yaitu memiliki akses ke dan akrab dengan kode perpustakaan.) Akhirnya pengguna akan menempatkan puluhan cek bersyarat atau membangun pembungkus sekitar perpustakaan untuk menjelaskan non-LSP (perilaku khusus kelas). Skenario kasus terburuk akan terjadi jika perpustakaan adalah produk komersial sumber tertutup.
rwong
@Andres dan rwong, tolong ilustrasikan masalah itu dengan jawaban. Jawaban yang diterima cukup banyak mendukung Paul Davies karena konsekuensinya tampak kecil (Pengecualian) yang akan dengan cepat diperhatikan dan diperbaiki jika Anda memiliki kompiler yang baik, penganalisa statis, atau tes unit minimal.
user949300

Jawaban:

31

Saya pikir itu dinyatakan dengan sangat baik dalam pertanyaan itu yang merupakan salah satu alasan yang terpilih sangat tinggi.

Sekarang ketika memanggil Tutup () pada Tugas, ada kemungkinan panggilan akan gagal jika itu adalah ProjectTask dengan status yang dimulai, ketika itu tidak akan terjadi jika itu adalah Tugas dasar.

Bayangkan jika Anda mau:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

Dalam metode ini, kadang-kadang panggilan .Close () akan meledak, jadi sekarang berdasarkan implementasi konkret dari tipe turunan Anda harus mengubah cara metode ini berperilaku dari bagaimana metode ini akan ditulis jika Tugas tidak memiliki subtipe yang bisa menjadi diserahkan ke metode ini.

Karena pelanggaran substitusi liskov, kode yang menggunakan tipe Anda harus memiliki pengetahuan eksplisit tentang cara kerja internal tipe turunan untuk memperlakukannya secara berbeda. Kode ini sangat erat berpasangan dan secara umum membuat implementasi lebih sulit untuk digunakan secara konsisten.

Jimmy Hoffa
sumber
Apakah itu berarti bahwa kelas anak tidak dapat memiliki metode publiknya sendiri yang tidak dideklarasikan di kelas induk?
Songo
@Songo: Tidak harus: bisa, tetapi metode-metode itu "tidak dapat dijangkau" dari penunjuk dasar (atau referensi atau variabel atau bahasa apa pun yang Anda gunakan menyebutnya) dan Anda memerlukan beberapa informasi jenis run-time untuk menanyakan jenis objek yang dimiliki sebelum Anda dapat memanggil fungsi-fungsi itu. Tetapi ini adalah masalah yang sangat terkait dengan sintaksis bahasa dan semantik.
Emilio Garavaglia
2
Tidak. Ini untuk ketika kelas anak direferensikan seolah-olah itu adalah jenis kelas induk, di mana anggota yang tidak dinyatakan dalam kelas induk tidak dapat diakses.
Chewy Gumball
1
@ Phil Yap; ini adalah definisi kopling ketat: Mengubah satu hal menyebabkan perubahan pada hal lain. Kelas yang digabungkan secara longgar dapat mengubah implementasinya tanpa mengharuskan Anda mengubah kode di luarnya. Inilah sebabnya mengapa kontrak itu baik, mereka memandu Anda bagaimana tidak memerlukan perubahan pada konsumen objek Anda: Penuhi kontrak dan konsumen tidak perlu modifikasi, sehingga kopling longgar tercapai. Ketika konsumen Anda perlu kode untuk implementasi Anda daripada kontrak Anda ini adalah kopling ketat, dan diperlukan saat melanggar LSP.
Jimmy Hoffa
1
@ user949300 Keberhasilan perangkat lunak apa pun untuk menyelesaikan pekerjaannya bukan ukuran kualitasnya, jangka panjang, atau biaya jangka pendek. Prinsip-prinsip desain adalah upaya membawa pedoman untuk mengurangi biaya jangka panjang perangkat lunak, bukan untuk membuat perangkat lunak "berfungsi". Orang dapat mengikuti semua prinsip yang mereka inginkan sementara masih gagal menerapkan solusi kerja, atau tidak mengikuti dan menerapkan solusi kerja. Meskipun koleksi java mungkin bekerja untuk banyak orang, itu tidak berarti biaya untuk bekerja dengan mereka dalam jangka panjang semurah mungkin.
Jimmy Hoffa
13

Jika Anda tidak memenuhi kontrak yang telah ditentukan dalam kelas dasar, semuanya dapat gagal saat Anda mendapatkan hasil yang tidak aktif.

LSP di negara bagian wikipedia

  • Prasyarat tidak dapat diperkuat dalam subtipe.
  • Postconditions tidak dapat dilemahkan dalam subtipe.
  • Invarian tipe supertipe harus dipertahankan dalam subtipe.

Jika ada yang tidak tahan, penelepon mungkin mendapatkan hasil yang tidak diharapkannya.

Zavior
sumber
1
Bisakah Anda memikirkan contoh konkret untuk menunjukkan ini?
Mark Booth
1
@MarkBooth Masalah lingkaran-elips / persegi panjang mungkin berguna untuk menunjukkannya; artikel wikipedia adalah tempat yang baik untuk memulai: en.wikipedia.org/wiki/Circle-ellipse_problem
Ed Hastings
7

Pertimbangkan kasus klasik dari catatan pertanyaan wawancara: Anda telah memperoleh Circle dari Ellipse. Mengapa? Karena sebuah lingkaran berbentuk elips, tentu saja!

Kecuali ... ellipse memiliki dua fungsi:

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

Jelas, ini harus didefinisikan ulang untuk Lingkaran, karena Lingkaran memiliki jari-jari yang seragam. Anda memiliki dua kemungkinan:

  1. Setelah memanggil set_alpha_radius atau set_beta_radius, keduanya diatur ke jumlah yang sama.
  2. Setelah memanggil set_alpha_radius atau set_beta_radius, objek tidak lagi menjadi Lingkaran.

Sebagian besar bahasa OO tidak mendukung yang kedua, dan untuk alasan yang baik: akan mengejutkan untuk menemukan bahwa Lingkaran Anda tidak lagi menjadi Lingkaran. Jadi opsi pertama adalah yang terbaik. Tetapi pertimbangkan fungsi berikut:

some_function(Ellipse byref e)

Bayangkan beberapa panggilan fungsi e.set_alpha_radius. Tetapi karena e benar - benar sebuah Circle, secara mengejutkan jari-jari beta-nya juga diatur.

Dan di sinilah letak prinsip substitusi: sebuah subclass harus dapat diganti dengan superclass. Kalau tidak, hal-hal mengejutkan terjadi.

Kaz Dragon
sumber
1
Saya pikir Anda dapat mengalami masalah jika Anda menggunakan objek yang bisa berubah. Lingkaran juga merupakan elips. Tetapi jika Anda mengganti elips yang juga lingkaran dengan elips lain (yang adalah apa yang Anda lakukan dengan menggunakan metode setter) tidak ada jaminan bahwa elips baru juga akan menjadi lingkaran (lingkaran adalah subset elips yang tepat).
Giorgio
2
Dalam dunia yang sepenuhnya fungsional (dengan objek yang tidak dapat diubah), metode set_alpha_radius (d) akan memiliki tipe elips kembali (baik di elips dan di kelas lingkaran).
Giorgio
@Iorgio Ya, saya seharusnya menyebutkan bahwa masalah ini hanya terjadi pada objek yang bisa berubah.
Kaz Dragon
@KazDragon: Mengapa ada orang yang mengganti elips dengan objek lingkaran ketika kita tahu bahwa elips BUKAN Lingkaran? Jika seseorang melakukan itu, mereka tidak memiliki pemahaman yang benar tentang entitas yang mereka coba modelkan. Tetapi dengan memperbolehkan substitusi ini, bukankah kita mendorong pemahaman yang longgar tentang sistem yang mendasarinya yang kita coba modelkan dalam perangkat lunak kita, dan dengan demikian menciptakan perangkat lunak yang buruk pada dasarnya?
maverick
@maverick Saya yakin Anda telah membaca hubungan yang saya jelaskan sebelumnya. Yang diusulkan adalah-hubungan adalah sebaliknya: lingkaran adalah elips. Secara khusus, lingkaran adalah elips di mana jari-jari alfa dan beta identik. Jadi, harapannya mungkin bahwa fungsi apa pun yang mengharapkan elips sebagai parameter bisa sama-sama mengambil lingkaran. Pertimbangkan calcul_area (Ellipse). Melewati lingkaran itu akan menghasilkan hasil yang sama. Tetapi masalahnya adalah bahwa perilaku fungsi mutasi Ellipse tidak dapat disubstitusikan dengan yang ada di Circle.
Kaz Dragon
6

Dalam kata-kata awam:

Kode Anda akan memiliki banyak sekali KASUS / saklar klausa di seluruh.

Setiap salah satu dari klausa KASUS / sakelar ini akan membutuhkan case baru yang ditambahkan dari waktu ke waktu, artinya basis kode tidak scalable dan dapat dikelola sebagaimana mestinya.

LSP memungkinkan kode bekerja lebih seperti perangkat keras:

Anda tidak perlu memodifikasi iPod Anda karena Anda membeli sepasang speaker eksternal baru, karena speaker eksternal lama dan baru menghargai antarmuka yang sama, mereka dapat dipertukarkan tanpa iPod kehilangan fungsionalitas yang diinginkan.

Tulains Córdova
sumber
2
-1: seluruh jawaban buruk
Thomas Eding
3
@ Thomas saya tidak setuju. Ini analogi yang bagus. Dia berbicara tentang tidak melanggar harapan, yang menjadi tujuan LSP. (meskipun bagian tentang case / switch agak lemah, saya setuju)
Andres F.
2
Dan kemudian Apple memecah LSP dengan mengubah konektor. Jawaban ini hidup terus.
Magus
Saya tidak mengerti apa kaitannya dengan pernyataan beralih dengan LSP. jika Anda mengacu pada peralihan typeof(someObject)untuk memutuskan apa yang "diizinkan untuk Anda lakukan", maka tentu saja, tetapi itu adalah anti-pola lain sama sekali.
sara
Pengurangan drastis dalam jumlah pernyataan switch adalah efek samping yang diinginkan dari LSP. Karena objek dapat berdiri untuk objek lain yang memperluas antarmuka yang sama, tidak perlu kasus khusus untuk diurus.
Tulains Córdova
1

untuk memberikan contoh kehidupan nyata dengan UndoManager java

itu mewarisi dari AbstractUndoableEdityang kontraknya menentukan bahwa ia memiliki 2 negara (dibatalkan dan redone) dan dapat pergi di antara mereka dengan satu panggilan ke undo()danredo()

namun UndoManager memiliki lebih banyak status dan bertindak seperti pembatalan pembatalan (setiap panggilan untuk undomembatalkan beberapa tetapi tidak semua suntingan, melemahkan postcondition)

ini mengarah ke situasi hipotetis di mana Anda menambahkan UndoManager ke CompoundEdit sebelum menelepon end()kemudian memanggil undo pada CompoundEdit yang akan mengarahkannya untuk memanggil undo()setiap pengeditan setelah membiarkan hasil edit Anda sebagian dibatalkan

Saya menggulung sendiri UndoManageruntuk menghindari itu (saya mungkin harus mengubah nama menjadi UndoBuffermeskipun)

ratchet freak
sumber
1

Contoh: Anda bekerja dengan kerangka UI, dan Anda membuat kontrol UI kustom Anda sendiri dengan mensubkelas Controlkelas dasar. Kelas Controldasar mendefinisikan metode getSubControls()yang harus mengembalikan koleksi kontrol bersarang (jika ada). Tetapi Anda mengganti metode untuk benar-benar mengembalikan daftar tanggal lahir presiden Amerika Serikat.

Jadi apa yang salah dengan ini? Jelas bahwa rendering kontrol akan gagal, karena Anda tidak mengembalikan daftar kontrol seperti yang diharapkan. Kemungkinan besar UI akan macet. Anda melanggar kontrak yang harus dipatuhi subclass Kontrol.

JacquesB
sumber
0

Anda juga dapat melihatnya dari sudut pandang pemodelan. Ketika Anda mengatakan bahwa instance kelas Ajuga merupakan instance kelas BAnda menyiratkan bahwa "perilaku yang dapat diamati dari instance kelas Ajuga dapat diklasifikasikan sebagai perilaku yang dapat diamati dari instance kelas B" (Ini hanya mungkin jika kelas Bkurang spesifik daripada kelas A.)

Jadi, melanggar LSP berarti ada beberapa kontradiksi dalam desain Anda: Anda mendefinisikan beberapa kategori untuk objek Anda dan kemudian Anda tidak menghargai mereka dalam implementasi Anda, sesuatu pasti salah.

Seperti membuat kotak dengan tag: "Kotak ini hanya berisi bola biru", dan kemudian melemparkan bola merah ke dalamnya. Apa gunanya tag seperti itu jika menunjukkan informasi yang salah?

Giorgio
sumber
0

Saya mewarisi basis kode baru-baru ini yang memiliki beberapa pelanggar Liskov besar di dalamnya. Di kelas-kelas penting. Ini telah menyebabkan saya sangat sakit. Biarkan saya jelaskan alasannya.

Saya punya Class A, yang berasal dari Class B. Class Adan Class Bberbagi banyak properti yang Class Amenimpa dengan implementasinya sendiri. Pengaturan atau mendapatkan Class Aproperti memiliki efek berbeda untuk pengaturan atau mendapatkan properti yang sama persis dari Class B.

public Class A
{
    public virtual string Name
    {
        get; set;
    }
}

Class B : A
{
    public override string Name
    {
        get
        {
            return TranslateName(base.Name);
        }
        set
        {
            base.Name = value;
            FunctionWithSideEffects();
        }
    }
}

Mengesampingkan fakta bahwa ini adalah cara yang benar-benar mengerikan untuk melakukan terjemahan dalam .NET, ada sejumlah masalah lain dengan kode ini.

Dalam hal Nameini digunakan sebagai indeks dan variabel kontrol aliran di sejumlah tempat. Kelas-kelas di atas berserakan di seluruh basis kode dalam bentuk mentah dan turunannya. Melanggar prinsip substitusi Liskov dalam hal ini berarti bahwa saya perlu mengetahui konteks setiap panggilan tunggal ke masing-masing fungsi yang mengambil kelas dasar.

Kode menggunakan objek keduanya Class Adan Class B, jadi saya tidak bisa membuat Class Aabstrak untuk memaksa orang untuk menggunakan Class B.

Ada beberapa fungsi utilitas yang sangat berguna yang beroperasi Class Adan fungsi utilitas lain yang sangat berguna yang beroperasi Class B. Idealnya saya ingin dapat menggunakan fungsi utilitas yang dapat beroperasi pada Class Apada Class B. Banyak fungsi yang mengambil a Class Bbisa dengan mudah diambil Class Ajika bukan karena pelanggaran LSP.

Hal terburuk tentang ini adalah bahwa kasus khusus ini sangat sulit untuk diperbaiki karena seluruh aplikasi bergantung pada dua kelas ini, beroperasi pada kedua kelas setiap saat dan akan pecah dalam ratusan cara jika saya mengubah ini (yang akan saya lakukan bagaimanapun).

Apa yang harus saya lakukan untuk memperbaikinya adalah membuat NameTranslatedproperti, yang akan menjadi Class Bversi Nameproperti dan sangat, sangat hati-hati mengubah setiap referensi ke Nameproperti yang diturunkan untuk menggunakan NameTranslatedproperti baru saya . Namun, jika salah satu dari referensi ini salah, seluruh aplikasi bisa meledak.

Mengingat basis kode tidak memiliki unit test di sekitarnya, ini hampir menjadi skenario paling berbahaya yang bisa dihadapi pengembang. Jika saya tidak mengubah pelanggaran, saya harus menghabiskan banyak energi mental untuk melacak jenis objek apa yang sedang dioperasikan pada setiap metode dan jika saya memperbaiki pelanggaran, saya bisa membuat seluruh produk meledak pada waktu yang tidak tepat.

Stephen
sumber
Apa yang akan terjadi jika dalam kelas turunan Anda membayangi properti yang diwariskan dengan hal yang berbeda yang memiliki nama yang sama [misalnya kelas bersarang] dan membuat pengidentifikasi baru BaseNamedan TranslatedNameuntuk mengakses kedua gaya kelas-A Namedan makna kelas-B? Maka setiap upaya untuk mengakses Namepada variabel tipe Bakan ditolak dengan kesalahan kompiler, sehingga Anda dapat memastikan bahwa semua referensi dikonversi ke salah satu bentuk lain.
supercat
Saya tidak lagi bekerja di tempat itu. Akan sangat canggung untuk memperbaikinya. :-)
Stephen
-4

Jika Anda ingin merasakan masalah pelanggaran LSP, pikirkan apa yang terjadi jika Anda hanya memiliki .dll / .jar kelas dasar (tidak ada kode sumber) dan Anda harus membangun kelas turunan baru. Anda tidak pernah bisa menyelesaikan tugas ini.

sgud
sumber
1
Ini hanya membuka lebih banyak pertanyaan daripada menjadi jawaban.
Frank