Mengapa Square yang mewarisi dari Rectangle menjadi bermasalah jika kita mengganti metode SetWidth dan SetHeight?

105

Jika kotak adalah jenis persegi panjang, lalu mengapa persegi tidak bisa mewarisi dari persegi panjang? Atau mengapa itu desain yang buruk?

Saya telah mendengar orang berkata:

Jika Anda membuat Square berasal dari Rectangle, maka Square harus dapat digunakan di mana saja yang Anda harapkan persegi panjang

Apa masalah yang terjadi di sini? Dan mengapa Square dapat digunakan di mana saja Anda mengharapkan persegi panjang? Itu hanya dapat digunakan jika kita membuat objek Square, dan jika kita menimpa metode SetWidth dan SetHeight untuk Square daripada mengapa ada masalah?

Jika Anda memiliki metode SetWidth dan SetHeight pada kelas dasar Rectangle Anda dan jika referensi Rectangle Anda menunjuk ke sebuah Square, maka SetWidth dan SetHeight tidak masuk akal karena pengaturan yang satu akan mengubah yang lain untuk mencocokkannya. Dalam kasus ini, Square gagal dalam Tes Substitusi Liskov dengan Rectangle dan abstraksi untuk memiliki Square diwarisi dari Rectangle adalah yang buruk.

Adakah yang bisa menjelaskan argumen di atas? Sekali lagi, jika kita over-naik metode SetWidth dan SetHeight di Square, bukankah itu akan menyelesaikan masalah ini?

Saya juga pernah mendengar / membaca:

Masalah sebenarnya adalah bahwa kita tidak memodelkan persegi panjang, melainkan "persegi panjang yang dibentuk kembali" yaitu, persegi panjang yang lebar atau tingginya dapat dimodifikasi setelah pembuatan (dan kami masih menganggapnya sebagai objek yang sama). Jika kita melihat kelas persegi panjang dengan cara ini, jelas bahwa persegi bukanlah "persegi panjang yang dibentuk kembali", karena persegi tidak dapat dibentuk kembali dan masih menjadi persegi (secara umum). Secara matematis, kita tidak melihat masalah karena ketidakstabilan bahkan tidak masuk akal dalam konteks matematika

Di sini saya percaya "re-sizeable" adalah istilah yang benar. Kotak adalah "re-sizeable" dan begitu juga kotak. Apakah saya kehilangan sesuatu dalam argumen di atas? Sebuah persegi dapat berukuran ulang seperti persegi panjang apa pun.

pengguna793468
sumber
15
Pertanyaan ini tampaknya sangat abstrak. Ada banyak trilyun cara untuk menggunakan kelas dan pewarisan, apakah membuat beberapa kelas mewarisi dari beberapa kelas adalah ide yang baik biasanya tergantung sebagian besar pada bagaimana Anda ingin menggunakan kelas-kelas itu. Tanpa kasus praktis saya tidak bisa melihat bagaimana pertanyaan ini bisa mendapatkan jawaban yang relevan.
aaaaaaaaaaaa
2
Menggunakan beberapa akal sehat, ingat bahwa persegi adalah persegi panjang, jadi jika objek dari kelas persegi Anda tidak dapat digunakan di mana persegi panjang diperlukan, mungkin beberapa cacat desain aplikasi.
Cthulhu
7
Saya pikir pertanyaan yang lebih baik adalah Why do we even need Square? Seperti memiliki dua pena. Satu pena biru dan satu pena merah biru, kuning atau hijau. Pena biru itu mubazir - bahkan lebih dalam kasus kotak karena tidak memiliki manfaat biaya.
Gusdor
2
@eBusiness Sifat abstraknya adalah apa yang membuatnya menjadi pertanyaan pembelajaran yang baik. Sangat penting untuk dapat mengenali penggunaan subtipe yang buruk secara independen dari kasus penggunaan tertentu.
Doval
5
@ Cthulhu Tidak juga. Subtyping adalah semua tentang perilaku dan kotak yang bisa berubah tidak berperilaku seperti kotak yang bisa berubah. Inilah sebabnya mengapa metafora "adalah ..." buruk.
Doval

Jawaban:

189

Pada dasarnya kami ingin hal-hal berperilaku bijaksana

Pertimbangkan masalah berikut:

Saya diberi sekelompok persegi panjang dan saya ingin menambah luasnya 10%. Jadi yang saya lakukan adalah saya mengatur panjang persegi panjang menjadi 1,1 kali dari sebelumnya.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

Sekarang dalam kasus ini, semua persegi panjang saya sekarang memiliki panjangnya meningkat 10%, yang akan meningkatkan area mereka sebesar 10%. Sayangnya, seseorang benar-benar telah memberi saya campuran kotak dan persegi panjang, dan ketika panjang persegi diubah, begitu pula lebarnya.

Tes unit saya lulus karena saya menulis semua tes unit saya untuk menggunakan kumpulan persegi panjang. Saya sekarang telah memperkenalkan bug halus ke dalam aplikasi saya yang dapat luput dari perhatian selama berbulan-bulan.

Lebih buruk lagi, Jim dari akuntansi melihat metode saya dan menulis beberapa kode lain yang menggunakan fakta bahwa jika ia memasukkan kotak ke dalam metode saya, ia mendapatkan peningkatan 21% dalam ukuran yang sangat bagus. Jim bahagia dan tidak ada yang lebih bijak.

Jim dipromosikan untuk pekerjaan luar biasa ke divisi yang berbeda. Alfred bergabung dengan perusahaan sebagai junior. Dalam laporan bug pertamanya, Jill dari Advertising telah melaporkan bahwa meneruskan kuadrat ke metode ini menghasilkan peningkatan 21% dan ingin bug diperbaiki. Alfred melihat bahwa Squares dan Rectangles digunakan di mana-mana dalam kode dan menyadari bahwa memutus rantai pewarisan tidak mungkin. Dia juga tidak memiliki akses ke kode sumber Akuntansi. Jadi Alfred memperbaiki bug seperti ini:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

Alfred senang dengan keterampilan meretas uber dan Jill menandatangani bahwa bug diperbaiki.

Bulan depan tidak ada yang dibayar karena Akuntansi bergantung pada kemampuan untuk lulus kuadrat ke IncreaseRectangleSizeByTenPercentmetode dan mendapatkan peningkatan area 21%. Seluruh perusahaan masuk ke mode "prioritas 1 bugfix" untuk melacak sumber masalah. Mereka melacak masalahnya sampai pada perbaikan Alfred. Mereka tahu bahwa mereka harus membuat Akuntansi dan Periklanan tetap bahagia. Jadi mereka memperbaiki masalah dengan mengidentifikasi pengguna dengan pemanggilan metode seperti:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

Dan seterusnya dan seterusnya.

Anekdot ini didasarkan pada situasi dunia nyata yang dihadapi programmer setiap hari. Pelanggaran terhadap prinsip Substitusi Liskov dapat menyebabkan bug yang sangat halus yang hanya dapat diambil bertahun-tahun setelah ditulis, dimana saat memperbaiki pelanggaran akan merusak banyak hal dan tidak memperbaikinya akan membuat marah klien terbesar Anda.

Ada dua cara realistis untuk memperbaiki masalah ini.

Cara pertama adalah membuat Rectangle tidak berubah. Jika pengguna Rectangle tidak dapat mengubah properti Panjang dan Lebar, masalah ini hilang. Jika Anda ingin Rectangle dengan panjang dan lebar yang berbeda, Anda membuat yang baru. Kotak dapat mewarisi dari persegi panjang dengan senang hati.

Cara kedua adalah memutus rantai warisan antara kotak dan persegi panjang. Jika sebuah persegi didefinisikan sebagai memiliki satu SideLengthproperti dan persegi panjang memiliki Lengthdan Widthproperti dan tidak ada warisan, tidak mungkin untuk secara tidak sengaja memecahkan sesuatu dengan mengharapkan persegi panjang dan mendapatkan persegi. Dalam istilah C #, Anda bisa sealkelas persegi panjang Anda, yang memastikan bahwa semua Persegi Panjang yang Anda dapatkan sebenarnya adalah Persegi Panjang.

Dalam hal ini, saya suka cara "objek abadi" untuk memperbaiki masalah. Identitas persegi panjang adalah panjang dan lebarnya. Masuk akal bahwa ketika Anda ingin mengubah identitas suatu objek, apa yang sebenarnya Anda inginkan adalah objek baru . Jika Anda kehilangan pelanggan lama dan mendapatkan pelanggan baru, Anda tidak mengubah Customer.Idbidang dari pelanggan lama ke yang baru, Anda membuat yang baru Customer.

Pelanggaran prinsip Pergantian Liskov adalah hal biasa di dunia nyata, sebagian besar karena banyak kode di luar sana ditulis oleh orang-orang yang tidak kompeten / di bawah tekanan waktu / tidak peduli / melakukan kesalahan. Itu bisa dan memang mengarah pada beberapa masalah yang sangat buruk. Dalam kebanyakan kasus, Anda lebih memilih komposisi daripada warisan .

Stephen
sumber
7
Liskov adalah satu hal, dan penyimpanan adalah masalah lain. Dalam sebagian besar implementasi, contoh Square yang mewarisi dari Rectangle akan membutuhkan ruang untuk menyimpan dua dimensi, meskipun hanya satu yang diperlukan.
el.pescado
29
Penggunaan cerita yang brilian untuk mengilustrasikan poin
Rory Hunter
29
Cerita yang bagus tapi saya tidak setuju. Kasus penggunaan adalah: mengubah area persegi panjang. Perbaikan harus menambahkan metode 'ChangeArea' yang dapat ditimpa ke kotak yang khusus di Square. Ini tidak akan memutus rantai pewarisan, memperjelas apa yang ingin dilakukan pengguna, dan tidak akan menyebabkan bug yang diperkenalkan oleh perbaikan yang Anda sebutkan (yang akan ditangkap di area pementasan yang tepat).
Roy T.
33
@ RoyT .: Mengapa sebuah Rectangle tahu cara mengatur wilayahnya? Itu properti yang seluruhnya berasal dari panjang dan lebar. Dan lebih tepatnya, dimensi mana yang harus diubah - panjang, lebar, atau keduanya?
cao
32
@ Roy T. Sangat menyenangkan untuk mengatakan bahwa Anda telah memecahkan masalah secara berbeda, tetapi faktanya ini adalah contoh - walaupun disederhanakan - dari situasi dunia nyata yang dihadapi pengembang setiap hari setiap hari ketika mempertahankan produk lawas. Dan bahkan jika Anda menerapkan metode itu, itu tidak akan menghentikan pewaris yang melanggar LSP dan memperkenalkan bug yang mirip dengan ini. Inilah sebabnya mengapa hampir setiap kelas dalam kerangka NET disegel.
Stephen
30

Jika semua objek Anda tidak berubah, tidak ada masalah. Setiap kotak juga merupakan persegi panjang. Semua properti dari Rectangle juga properti dari sebuah Square.

Masalahnya dimulai ketika Anda menambahkan kemampuan untuk memodifikasi objek. Atau sungguh - ketika Anda mulai menyampaikan argumen ke objek, tidak hanya membaca getter properti.

Ada modifikasi yang dapat Anda lakukan untuk Rectangle yang mempertahankan semua invarian kelas Rectangle Anda, tetapi tidak semua invarian Square - seperti mengubah lebar atau tinggi. Tiba-tiba perilaku Rectangle tidak hanya sifat-sifatnya, itu juga kemungkinan modifikasi. Bukan hanya apa yang Anda dapatkan dari Rectangle, itu juga apa yang bisa Anda masukkan .

Jika Persegi Panjang Anda memiliki metode setWidthyang didokumentasikan sebagai mengubah lebar dan tidak mengubah ketinggian, maka Persegi tidak dapat memiliki metode yang kompatibel. Jika Anda mengubah lebar dan bukan tinggi, hasilnya bukan lagi kotak yang valid. Jika Anda memilih untuk memodifikasi lebar dan tinggi Square saat menggunakan setWidth, Anda tidak menerapkan spesifikasi Rectangle setWidth. Anda tidak bisa menang.

Saat Anda melihat apa yang dapat Anda "masukkan ke dalam" Persegi Panjang dan Kotak, pesan apa yang dapat Anda kirim kepada mereka, Anda mungkin akan menemukan bahwa pesan apa pun yang dapat Anda kirim secara sah ke Kotak, Anda juga dapat mengirim ke Kotak.

Ini masalah ko-varians vs kontra-varians.

Metode subclass yang tepat, dimana instance dapat digunakan dalam semua kasus di mana superclass diharapkan, mengharuskan setiap metode untuk:

  • Hanya kembalikan nilai yang akan dikembalikan oleh superclass - yaitu, tipe pengembalian harus merupakan subtipe dari tipe pengembalian metode superclass. Return adalah co-varian.
  • Terima semua nilai yang akan diterima supertype - yaitu, tipe argumen harus supertypes dari tipe argumen metode superclass. Argumen bersifat kontra-varian.

Jadi, kembali ke Rectangle dan Square: Apakah Square dapat menjadi subclass dari Rectangle sepenuhnya tergantung pada metode apa yang dimiliki Rectangle.

Jika Rectangle memiliki setter individu untuk lebar dan tinggi, Kotak tidak akan menjadi subclass yang baik.

Demikian juga, jika Anda membuat beberapa metode menjadi co-varian dalam argumen, seperti memiliki compareTo(Rectangle)di Rectangle dan compareTo(Square)di Square, Anda akan memiliki masalah menggunakan Square sebagai Rectangle.

Jika Anda mendesain Square dan Rectangle Anda agar kompatibel, itu kemungkinan akan berhasil, tetapi mereka harus dikembangkan bersama, atau saya berani bertaruh bahwa itu tidak akan berhasil.

Tuan
sumber
"Jika semua objek Anda tidak berubah, tidak ada masalah" - ini tampaknya pernyataan yang tidak relevan dalam konteks pertanyaan ini, yang secara eksplisit menyebut setter untuk lebar dan tinggi
nyamuk
11
Saya menemukan ini menarik, bahkan ketika itu "tampaknya tidak relevan"
Jesvin Jose
14
@gnat saya berpendapat itu relevan karena nilai sebenarnya dari pertanyaan adalah mengenali ketika ada hubungan subtyping yang valid antara dua jenis. Itu tergantung pada operasi yang menyatakan supertype, jadi ada baiknya menunjukkan masalah hilang jika metode mutator pergi.
Doval
1
@gnat juga, setter adalah mutator , jadi lrn pada dasarnya mengatakan, "Jangan lakukan itu dan itu tidak masalah." Saya kebetulan setuju dengan kekekalan untuk tipe sederhana, tetapi Anda membuat poin yang bagus: Untuk objek yang kompleks, masalahnya tidak begitu sederhana.
Patrick M
1
Pertimbangkan seperti ini, apa perilaku yang dijamin oleh kelas 'Rectangle'? Anda dapat mengubah lebar dan tinggi secara independen satu sama lain. (yaitu setWidth dan setHeight) metode. Sekarang jika Square diturunkan dari Rectangle, Square harus menjamin perilaku ini. Karena kuadrat tidak dapat menjamin perilaku ini, itu adalah warisan yang buruk. Namun, jika metode setWidth / setHeight dihapus dari kelas Rectangle, maka tidak ada perilaku seperti itu dan karenanya Anda bisa mendapatkan kelas Square dari Rectangle.
Nitin Bhide
17

Ada banyak jawaban bagus di sini; Jawaban Stephen secara khusus melakukan pekerjaan yang baik untuk menggambarkan mengapa pelanggaran prinsip substitusi menyebabkan konflik dunia nyata antara tim.

Saya pikir saya mungkin berbicara secara singkat tentang masalah khusus persegi panjang dan bujur sangkar, daripada menggunakannya sebagai metafora untuk pelanggaran lain pada LSP.

Ada masalah tambahan dengan square-is-a-special-of-rectangle yang jarang disebutkan, dan itu adalah: mengapa kita berhenti dengan kotak dan persegi panjang ? Jika kita mau mengatakan bahwa persegi adalah jenis persegi panjang khusus maka tentunya kita juga harus bersedia untuk mengatakan:

  • Kotak adalah jenis khusus belah ketupat - itu adalah belah ketupat dengan sudut persegi.
  • Belah ketupat adalah jenis jajaran genjang khusus - jajar genjang dengan sisi yang sama.
  • Persegi panjang adalah jenis jajaran genjang khusus - itu jajar genjang dengan sudut persegi
  • Persegi panjang, bujur sangkar, dan jajar genjang semuanya adalah jenis khusus trapesium - mereka adalah trapesium dengan dua set sisi paralel
  • Semua hal di atas adalah jenis segiempat khusus
  • Semua di atas adalah jenis khusus bentuk planar
  • Dan seterusnya; Saya bisa terus berjalan selama beberapa waktu di sini.

Apa hubungan semua hubungan di sini? Bahasa berbasis warisan kelas seperti C # atau Java tidak dirancang untuk mewakili jenis hubungan yang kompleks dengan berbagai jenis kendala. Sebaiknya hindari pertanyaan sepenuhnya dengan tidak mencoba mewakili semua hal ini sebagai kelas dengan hubungan subtipe.

Eric Lippert
sumber
3
Jika bentuk objek tidak berubah, maka seseorang dapat memiliki IShapejenis yang mencakup kotak pembatas, dan dapat digambar, diskalakan, dan diserialisasi, dan memiliki IPolygonsubtipe dengan metode untuk melaporkan jumlah simpul dan metode untuk mengembalikan sebuah IEnumerable<Point>. Seseorang kemudian dapat memiliki IQuadrilateralsubtipe yang berasal dari IPolygon, IRhombusdan IRectangle, berasal dari itu, dan ISquareberasal dari IRhombusdan IRectangle. Mutability akan membuang semuanya ke luar jendela, dan multiple inheritance tidak bekerja dengan kelas, tapi saya pikir tidak masalah dengan antarmuka yang tidak dapat diubah.
supercat
Saya secara efektif tidak setuju dengan Eric di sini (meskipun tidak cukup untuk -1!). Semua hubungan itu (mungkin) relevan, seperti yang disebutkan oleh @supercat; itu hanya masalah YAGNI: Anda tidak mengimplementasikannya sampai Anda membutuhkannya.
Mark Hurd
Jawaban yang sangat bagus Harus jauh lebih tinggi.
andrew.fox
1
@MarkHurd - ini bukan masalah YAGNI: hierarki berbasis warisan yang diusulkan berbentuk seperti taksonomi yang dijelaskan, tetapi tidak dapat ditulis untuk menjamin hubungan yang mendefinisikannya. Bagaimana IRhombusjaminan bahwa semua Pointdikembalikan dari yang Enumerable<Point>ditentukan IPolygonsesuai dengan tepi dengan panjang yang sama? Karena implementasi IRhombusantarmuka saja tidak menjamin bahwa objek konkret adalah-belah ketupat, warisan tidak bisa menjadi jawabannya.
A. Rager
14

Dari perspektif matematika, kotak adalah-persegi panjang. Jika seorang ahli matematika memodifikasi kotak sehingga tidak lagi mematuhi kontrak persegi, itu berubah menjadi persegi panjang.

Namun dalam desain OO, ini merupakan masalah. Objek adalah apa adanya, dan ini termasuk perilaku serta keadaan. Jika saya memegang objek persegi, tetapi orang lain mengubahnya menjadi persegi panjang, itu melanggar kontrak persegi bukan karena kesalahan saya sendiri. Ini menyebabkan segala macam hal buruk terjadi.

Faktor kunci di sini adalah mutabilitas . Bisakah bentuk berubah setelah dibangun?

  • Mutable: jika bentuk dibiarkan berubah begitu dibangun, persegi tidak dapat memiliki hubungan is-a dengan persegi panjang. Kontrak persegi panjang mencakup batasan bahwa sisi yang berlawanan harus memiliki panjang yang sama, tetapi sisi yang berdekatan tidak harus. Kotak harus memiliki empat sisi yang sama. Memodifikasi persegi melalui antarmuka persegi panjang dapat melanggar kontrak persegi.

  • Abadi: jika bentuk tidak dapat berubah setelah dibangun, maka objek persegi juga harus selalu memenuhi kontrak persegi panjang. Kotak dapat memiliki hubungan is-a dengan persegi panjang.

Dalam kedua kasus itu mungkin untuk meminta kotak untuk menghasilkan bentuk baru berdasarkan kondisinya dengan satu atau lebih perubahan. Sebagai contoh, orang bisa mengatakan "buat persegi panjang baru berdasarkan kotak ini, kecuali sisi yang berlawanan A dan C dua kali lebih panjang." Karena objek baru sedang dibangun, alun-alun asli terus mematuhi kontraknya.


sumber
1
This is one of those cases where the real world is not able to be modeled in a computer 100%. Kenapa begitu? Kita masih bisa memiliki model fungsional persegi dan persegi panjang. Satu-satunya konsekuensi adalah kita harus mencari konstruk yang lebih sederhana untuk abstrak atas dua objek tersebut.
Simon Bergot
6
Ada lebih banyak kesamaan antara persegi panjang dan kotak dari itu. Masalahnya adalah bahwa identitas persegi panjang dan identitas persegi adalah panjang sisinya (dan sudut di setiap persimpangan). Solusi terbaik di sini adalah membuat kuadrat mewarisi dari persegi panjang, tetapi membuat keduanya tidak berubah.
Stephen
3
@Stephen Setuju. Sebenarnya, menjadikan mereka tidak berubah adalah hal yang masuk akal untuk dilakukan terlepas dari masalah subtyping. Tidak ada alasan untuk membuatnya bisa berubah - tidak sulit untuk membuat kotak atau persegi panjang baru daripada untuk memutasikannya, jadi mengapa membuka kaleng cacing itu? Sekarang Anda tidak perlu khawatir tentang aliasing / efek samping dan Anda dapat menggunakannya sebagai kunci untuk peta / dicts jika diperlukan. Beberapa akan mengatakan "kinerja", yang akan saya katakan "optimasi prematur" sampai mereka benar-benar mengukur dan membuktikan bahwa hot spot ada dalam kode bentuk.
Doval
Maaf teman, sudah terlambat dan saya sangat lelah ketika saya menulis jawabannya. Saya menulis ulang untuk mengatakan apa yang sebenarnya saya maksudkan, yang intinya adalah kemampuan berubah-ubah.
13

Dan mengapa Square dapat digunakan di mana saja Anda mengharapkan persegi panjang?

Karena itulah bagian dari makna subtipe (lihat juga: Prinsip substitusi Liskov). Anda dapat melakukannya, harus dapat melakukan ini:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

Anda benar-benar melakukan ini sepanjang waktu (kadang-kadang bahkan lebih implisit) ketika menggunakan OOP.

dan jika kita over-naik metode SetWidth dan SetHeight untuk Square daripada mengapa ada masalah?

Karena Anda tidak dapat menimpanya dengan masuk akal Square. Karena sebuah persegi tidak dapat "berukuran ulang seperti persegi panjang". Ketika ketinggian persegi panjang berubah, lebarnya tetap sama. Tetapi ketika ketinggian kotak berubah, lebar harus berubah sesuai. Masalahnya bukan hanya ukurannya yang besar, tetapi ukurannya yang besar di kedua dimensi secara mandiri.

Deduplicator
sumber
Dalam banyak bahasa, Anda bahkan tidak perlu Rect r = s;saluran, Anda bisa saja doSomethingWith(s)dan runtime akan menggunakan panggilan apa pun suntuk menyelesaikan Squaremetode virtual apa pun .
Patrick M
1
@PatrickM Anda tidak memerlukannya dalam bahasa apa pun yang memiliki subtyping. Saya memasukkan garis itu untuk eksposisi, untuk menjadi eksplisit.
Jadi timpa setWidthdan setHeightuntuk mengubah lebar dan tinggi.
ApproachingDarknessFish
@ValekHalfHeart Itulah opsi yang saya pertimbangkan.
7
@ValekHalfHeart: Justru itu pelanggaran Prinsip Pergantian Liskov yang akan menghantui Anda dan membuat Anda menghabiskan beberapa malam tanpa tidur mencoba menemukan bug aneh dua tahun kemudian ketika Anda lupa bagaimana kode itu seharusnya bekerja.
Jan Hudec
9

Apa yang Anda jelaskan bertentangan dengan apa yang disebut Prinsip Pergantian Liskov . Ide dasar dari LSP adalah bahwa setiap kali Anda menggunakan instance dari kelas tertentu, Anda harus selalu dapat menukar dengan instance dari setiap subkelas dari kelas itu, tanpa memperkenalkan bug.

Masalah Rectangle-Square sebenarnya bukan cara yang sangat baik untuk memperkenalkan Liskov. Ia mencoba menjelaskan prinsip luas menggunakan contoh yang sebenarnya cukup halus, dan berjalan bertabrakan dengan salah satu definisi intuitif paling umum dalam semua matematika. Ada yang menyebutnya masalah Ellipse-Circle karena alasan itu, tetapi sejauh ini hanya sedikit lebih baik. Pendekatan yang lebih baik adalah mengambil sedikit langkah mundur, menggunakan apa yang saya sebut masalah Parallelogram-Rectangle. Ini membuat banyak hal lebih mudah untuk dipahami.

Jajar genjang adalah segi empat dengan dua pasang sisi paralel. Ini juga memiliki dua pasang sudut yang kongruen. Tidak sulit membayangkan objek Paralelogram di sepanjang baris ini:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Salah satu cara umum untuk memikirkan persegi panjang adalah sebagai jajaran genjang dengan sudut kanan. Pada pandangan pertama, ini mungkin membuat Rectangle menjadi kandidat yang baik untuk mewarisi dari Parallelogram , sehingga Anda dapat menggunakan kembali semua kode yummy itu. Namun:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Mengapa kedua fungsi ini memperkenalkan bug di Rectangle? Masalahnya adalah bahwa Anda tidak dapat mengubah sudut dalam persegi panjang : mereka didefinisikan sebagai selalu 90 derajat, dan antarmuka ini sebenarnya tidak berfungsi untuk Rectangle yang diwarisi dari Parallelogram. Jika saya menukar Rectangle ke dalam kode yang mengharapkan Parallelogram, dan kode itu mencoba mengubah sudut, hampir pasti akan ada bug. Kami telah mengambil sesuatu yang dapat ditulisi di subclass dan membuatnya hanya-baca, dan itu merupakan pelanggaran Liskov.

Sekarang, bagaimana ini berlaku untuk Kotak dan Persegi Panjang?

Ketika kami mengatakan bahwa Anda dapat menetapkan nilai, kami biasanya mengartikan sesuatu yang sedikit lebih kuat daripada sekadar menuliskan nilai ke dalamnya. Kami menyiratkan tingkat eksklusivitas tertentu: jika Anda menetapkan nilai, kemudian melarang beberapa keadaan luar biasa, itu akan tetap pada nilai itu sampai Anda menetapkannya lagi. Ada banyak kegunaan untuk nilai-nilai yang dapat ditulis tetapi tidak tetap ditetapkan, tetapi ada juga banyak kasus yang bergantung pada nilai tetap di tempat itu setelah Anda mengaturnya. Dan di situlah kita mengalami masalah lain.

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Kelas Square kami mewarisi bug dari Rectangle, tetapi ada beberapa bug baru. Masalah dengan setSideA dan setSideB adalah bahwa tidak satu pun dari ini benar-benar dapat diselesaikan lagi: Anda masih dapat menulis nilai ke salah satu dari keduanya, tetapi akan berubah dari bawah Anda jika yang lain ditulis. Jika saya menukar ini dengan Parallelogram dalam kode yang tergantung pada kemampuan untuk mengatur sisi secara independen satu sama lain, itu akan menjadi aneh.

Itulah masalahnya, dan itulah sebabnya ada masalah dengan menggunakan Rectangle-Square sebagai pengantar Liskov. Rectangle-Square tergantung pada perbedaan antara bisa menulis ke sesuatu dan bisa mengaturnya , dan itu perbedaan yang jauh lebih halus daripada bisa mengatur sesuatu dibandingkan membuatnya hanya bisa dibaca-saja. Rectangle-Square masih memiliki nilai sebagai contoh, karena mendokumentasikan gotcha yang cukup umum yang harus diwaspadai, tetapi tidak boleh digunakan sebagai contoh pengantar . Biarkan pelajar mendapatkan dasar-dasar terlebih dahulu, dan kemudian melemparkan sesuatu yang lebih keras pada mereka.

Spooniest
sumber
8

Subtyping adalah tentang perilaku.

Untuk tipe Bmenjadi subtipe tipe A, itu harus mendukung setiap operasi yang mengetik Amendukung dengan semantik yang sama (pembicaraan mewah untuk "perilaku"). Menggunakan alasan bahwa setiap B adalah A tidak tidak bekerja - kompatibilitas perilaku memiliki kata akhir. Sebagian besar waktu "B adalah sejenis A" tumpang tindih dengan "B berperilaku seperti A", tetapi tidak selalu .

Sebuah contoh:

Pertimbangkan himpunan bilangan real. Dalam bahasa apapun, kita bisa mengharapkan mereka untuk mendukung operasi +, -, *, dan /. Sekarang pertimbangkan himpunan bilangan bulat positif ({1, 2, 3, ...}). Jelas, setiap bilangan bulat positif juga bilangan real. Tetapi apakah tipe bilangan bulat positif merupakan subtipe dari tipe bilangan real? Mari kita lihat empat operasi dan lihat apakah bilangan bulat positif berperilaku sama seperti bilangan real:

  • +: Kami dapat menambahkan bilangan bulat positif tanpa masalah.
  • -: Tidak semua pengurangan bilangan bulat positif menghasilkan bilangan bulat positif. Misalnya 3 - 5.
  • *: Kita dapat melipatgandakan bilangan bulat positif tanpa masalah.
  • /: Kami tidak selalu dapat membagi bilangan bulat positif dan mendapatkan bilangan bulat positif. Misalnya 5 / 3.

Jadi, meskipun bilangan bulat positif menjadi subset dari bilangan real, mereka bukan subtipe. Argumen serupa dapat dibuat untuk bilangan bulat ukuran terbatas. Jelas setiap integer 32-bit juga merupakan integer 64-bit, tetapi 32_BIT_MAX + 1akan memberi Anda hasil yang berbeda untuk setiap jenis. Jadi jika saya memberi Anda beberapa program dan Anda mengubah jenis setiap variabel integer 32-bit menjadi integer 64-bit, ada kemungkinan program akan berperilaku berbeda (yang hampir selalu berarti salah ).

Tentu saja, Anda dapat menetapkan +untuk int 32-bit sehingga hasilnya adalah integer 64-bit, tetapi sekarang Anda harus mencadangkan 64 bit ruang setiap kali Anda menambahkan dua angka 32-bit. Itu mungkin atau mungkin tidak dapat Anda terima tergantung pada kebutuhan memori Anda.

Mengapa ini penting?

Penting bagi program untuk menjadi benar. Ini bisa dibilang properti paling penting untuk dimiliki sebuah program. Jika suatu program benar untuk beberapa jenis A, satu-satunya cara untuk menjamin bahwa program akan terus benar untuk beberapa subtipe Badalah jika Bberperilaku seperti Adalam segala hal.

Jadi Anda memiliki tipe Rectangles, yang spesifikasinya mengatakan sisi-sisinya dapat diubah secara independen. Anda menulis beberapa program yang menggunakan Rectanglesdan menganggap implementasi mengikuti spesifikasi. Kemudian Anda memperkenalkan subtipe yang disebut Squaresisi mana yang tidak dapat diubah ukurannya secara independen. Akibatnya, sebagian besar program yang mengubah ukuran persegi panjang sekarang akan salah.

Doval
sumber
6

Jika kotak adalah jenis persegi panjang daripada mengapa sebuah persegi tidak bisa mewarisi dari persegi panjang? Atau mengapa itu desain yang buruk?

Hal pertama yang pertama, tanyakan pada diri sendiri mengapa Anda berpikir persegi adalah persegi panjang.

Tentu saja kebanyakan orang mengetahui hal itu di sekolah dasar, dan itu akan tampak jelas. Persegi panjang adalah bentuk 4 sisi dengan sudut 90 derajat, dan kotak memenuhi semua sifat itu. Jadi bukankah persegi adalah persegi panjang?

Masalahnya adalah bahwa itu semua tergantung pada apa kriteria awal Anda untuk mengelompokkan objek, konteks apa yang Anda lihat pada objek-objek ini. Dalam bentuk geometri diklasifikasikan berdasarkan sifat titik, garis, dan malaikatnya.

Jadi sebelum Anda bahkan mengatakan "persegi adalah jenis persegi panjang" Anda harus bertanya pada diri sendiri, apakah ini berdasarkan kriteria yang saya pedulikan .

Dalam sebagian besar kasus itu tidak akan menjadi apa yang Anda pedulikan sama sekali. Mayoritas sistem yang memodelkan bentuk, seperti GUI, grafik, dan permainan video, tidak terutama berkaitan dengan pengelompokan geometris suatu objek tetapi itu adalah perilaku. Pernahkah Anda bekerja pada sistem yang penting bahwa persegi adalah jenis persegi panjang dalam arti geometris. Apa yang bahkan memberi Anda, mengetahui bahwa ia memiliki 4 sisi dan sudut 90 derajat?

Anda tidak memodelkan sistem statis, Anda memodelkan sistem dinamis di mana hal-hal akan terjadi (bentuk akan dibuat, dihancurkan, diubah, digambar dll). Dalam konteks ini Anda peduli tentang perilaku bersama antara objek, karena perhatian utama Anda adalah apa yang dapat Anda lakukan dengan bentuk, aturan apa yang harus dipertahankan untuk tetap memiliki sistem yang koheren.

Dalam konteks ini sebuah persegi jelas bukan persegi panjang , karena aturan yang mengatur bagaimana persegi dapat diubah tidak sama dengan persegi panjang. Jadi mereka bukan tipe yang sama.

Dalam hal ini jangan memodelkannya seperti itu. Mengapa kamu akan? Ini memberi Anda apa-apa selain pembatasan yang tidak perlu.

Itu hanya dapat digunakan jika kita membuat objek Square, dan jika kita menimpa metode SetWidth dan SetHeight untuk Square daripada mengapa ada masalah?

Jika Anda melakukannya meskipun Anda secara praktis menyatakan dalam kode bahwa mereka bukan hal yang sama. Kode Anda akan mengatakan persegi berperilaku seperti ini dan persegi panjang berperilaku seperti itu tetapi mereka tetap sama.

Mereka jelas tidak sama dalam konteks yang Anda pedulikan karena Anda baru saja mendefinisikan dua perilaku yang berbeda. Jadi mengapa berpura-pura mereka sama jika mereka hanya mirip dalam konteks yang tidak Anda pedulikan?

Ini menyoroti masalah signifikan ketika pengembang datang ke domain yang ingin mereka modelkan. Sangat penting untuk memperjelas konteks apa yang Anda minati sebelum Anda mulai berpikir tentang objek dalam domain. Aspek apa yang Anda minati. Ribuan tahun yang lalu orang-orang Yunani memedulikan sifat-sifat bersama dari garis dan malaikat bentuk, dan mengelompokkannya berdasarkan hal-hal ini. Itu tidak berarti Anda dipaksa untuk melanjutkan pengelompokan itu jika bukan itu yang Anda pedulikan (yang dalam 99% pemodelan waktu dalam perangkat lunak Anda tidak akan peduli).

Banyak jawaban untuk pertanyaan ini berfokus pada sub-pengetikan tentang perilaku pengelompokan karena mereka aturan .

Tetapi sangat penting untuk memahami bahwa Anda tidak melakukan ini hanya untuk mengikuti aturan. Anda melakukan ini karena dalam sebagian besar kasus inilah yang sebenarnya Anda pedulikan. Anda tidak peduli jika persegi dan persegi panjang memiliki malaikat internal yang sama. Anda peduli tentang apa yang bisa mereka lakukan saat masih menjadi kotak dan persegi panjang. Anda peduli dengan perilaku objek karena Anda memodelkan sistem yang berfokus pada perubahan sistem berdasarkan aturan perilaku objek.

Cormac Mulhall
sumber
Jika variabel tipe Rectanglehanya digunakan untuk merepresentasikan nilai , maka dimungkinkan bagi suatu kelas Squareuntuk mewarisi dari Rectangledan sepenuhnya mematuhi kontraknya. Sayangnya, banyak bahasa tidak membuat perbedaan antara variabel yang merangkum nilai dan yang mengidentifikasi entitas.
supercat
Mungkin, tapi kenapa harus repot-repot? Inti dari masalah persegi panjang / bujur sangkar bukanlah untuk mencoba dan mencari cara untuk membuat hubungan "a square is a rectangle", tetapi untuk menyadari bahwa hubungan itu tidak benar-benar ada dalam konteks bahwa Anda menggunakan objek (secara perilaku), dan sebagai peringatan tentang tidak memaksakan hubungan yang tidak relevan ke domain Anda.
Cormac Mulhall
Atau dengan kata lain: Jangan mencoba dan menekuk sendok. Itu tidak mungkin. Alih-alih hanya berusaha menyadari kebenaran, bahwa tidak ada sendok. :-)
Cormac Mulhall
1
Memiliki Squaretipe yang tidak dapat diubah yang mewarisi dari Rectnagletipe yang tidak dapat diubah dapat berguna jika ada beberapa jenis operasi yang hanya dapat dilakukan pada kotak. Sebagai contoh konsep yang realistis, pertimbangkan ReadableMatrixtipe [tipe dasar array persegi panjang yang dapat disimpan dengan berbagai cara, termasuk jarang], dan ComputeDeterminantmetode. Mungkin masuk akal untuk ComputeDeterminantbekerja hanya dengan ReadableSquareMatrixjenis yang berasal dari ReadableMatrix, yang saya anggap sebagai contoh yang Squareberasal dari a Rectangle.
supercat
5

Jika kotak adalah jenis persegi panjang daripada mengapa sebuah persegi tidak bisa mewarisi dari persegi panjang?

Masalahnya terletak pada pemikiran bahwa jika hal-hal terkait dalam beberapa cara dalam kenyataan, mereka harus terkait dengan cara yang persis sama setelah pemodelan.

Hal paling penting dalam pemodelan adalah mengidentifikasi atribut umum dan perilaku umum, mendefinisikannya di kelas dasar dan menambahkan atribut tambahan di kelas anak.

Masalah dengan contoh Anda adalah, itu sepenuhnya abstrak. Selama tidak ada yang tahu, untuk apa Anda berencana menggunakan kelas itu, sulit menebak desain apa yang harus Anda buat. Tetapi jika Anda benar-benar hanya ingin memiliki tinggi, lebar dan ukuran, itu akan lebih logis untuk:

  • mendefinisikan Square sebagai kelas dasar, dengan widthparameter dan resize(double factor)mengubah ukuran lebar dengan faktor yang diberikan
  • mendefinisikan kelas Rectangle dan subclass dari Square, karena ia menambahkan atribut lain height,, dan mengesampingkan resizefungsinya, yang memanggil super.resizedan kemudian mengubah ukuran ketinggian dengan faktor yang diberikan

Dari sudut pandang pemrograman, tidak ada dalam Square, yang tidak dimiliki Rectangle. Tidak ada gunanya membuat Kotak sebagai subclass dari Rectangle.

Pelaut Danubia
sumber
+1 Hanya karena kuadrat adalah jenis rect khusus dalam matematika, tidak berarti sama dengan OO.
Lovis
1
Persegi adalah persegi dan persegi panjang adalah persegi panjang. Hubungan di antara mereka juga harus ada dalam pemodelan, atau Anda memiliki model yang sangat buruk. Masalah sebenarnya adalah: 1) jika Anda membuatnya bisa berubah, Anda tidak lagi memodelkan kotak dan persegi panjang; 2) dengan asumsi bahwa hanya karena beberapa hubungan "adalah" berlaku di antara dua jenis objek, Anda dapat menggantikan satu dengan yang lainnya tanpa pandang bulu.
Doval
4

Karena oleh LSP, menciptakan hubungan warisan antara keduanya dan mengesampingkan setWidthdan setHeightuntuk memastikan kuadrat memiliki keduanya sama-sama memperkenalkan perilaku yang membingungkan dan tidak intuitif. Katakanlah kita memiliki kode:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

Tetapi jika metode createRectanglekembali Square, karena itu mungkin berkat Squaremewarisi dari Rectange. Kemudian harapan itu rusak. Di sini, dengan kode ini, kami berharap pengaturan lebar atau tinggi hanya akan menyebabkan perubahan lebar atau tinggi masing-masing. Maksud dari OOP adalah bahwa ketika Anda bekerja dengan superclass, Anda tidak memiliki pengetahuan tentang subkelas di bawahnya. Dan jika subclass mengubah perilaku sehingga bertentangan dengan harapan yang kita miliki tentang superclass, maka ada kemungkinan besar bug akan terjadi. Dan bug semacam itu sulit di-debug dan diperbaiki.

Salah satu ide utama tentang OOP adalah bahwa itu adalah perilaku, bukan data yang diwarisi (yang juga merupakan salah satu kesalahpahaman utama IMO). Dan jika Anda melihat persegi dan persegi panjang, mereka tidak memiliki perilaku sendiri yang bisa kita hubungkan dalam hubungan pewarisan.

Euforia
sumber
2

Yang dikatakan LSP adalah bahwa apa pun yang mewarisi dari Rectangleharus a Rectangle. Artinya, ia harus melakukan apa pun yang Rectangledilakukannya.

Mungkin dokumentasi untuk Rectangleditulis untuk mengatakan bahwa perilaku seorang Rectanglebernama radalah sebagai berikut:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Jika Kotak Anda tidak memiliki perilaku yang sama maka tidak berperilaku seperti Rectangle. Jadi LSP mengatakan itu tidak boleh diwarisi dari Rectangle. Bahasa tidak dapat menegakkan aturan ini, karena itu tidak bisa menghentikan Anda melakukan sesuatu yang salah dalam metode override, tetapi itu tidak berarti "tidak apa-apa karena bahasa ini memungkinkan saya mengganti metode" adalah argumen yang meyakinkan untuk melakukannya!

Sekarang, mungkin untuk menulis dokumentasi Rectanglesedemikian rupa sehingga tidak menyiratkan bahwa kode di atas mencetak 10, dalam hal ini mungkin Anda Squarebisa menjadi Rectangle. Anda mungkin melihat dokumentasi yang mengatakan sesuatu seperti, "ini tidak X. Selanjutnya, implementasi di kelas ini tidak Y". Jika demikian maka Anda memiliki kasus yang bagus untuk mengekstraksi antarmuka dari kelas, dan membedakan antara apa yang dijamin antarmuka, dan apa yang dijamin oleh kelas di samping itu. Tetapi ketika orang mengatakan "kotak yang bisa berubah bukan persegi yang bisa berubah, sedangkan kotak yang tidak bisa diubah adalah kotak yang tidak bisa diubah", mereka pada dasarnya mengasumsikan bahwa di atas memang bagian dari definisi yang wajar dari kotak yang bisa diubah.

Steve Jessop
sumber
ini sepertinya hanya mengulangi poin yang dijelaskan dalam jawaban yang diposting 5 jam yang lalu
agas
@gnat: apakah Anda ingin saya mengedit jawaban yang lain hingga kira-kira singkatnya? ;-) Saya tidak berpikir saya bisa tanpa menghilangkan poin yang menurut penjawab lain mungkin diperlukan untuk menjawab pertanyaan dan saya rasa tidak.
Steve Jessop
1

Subtipe dan, dengan ekstensi, pemrograman OO, sering mengandalkan Prinsip Pergantian Liskov, bahwa nilai apa pun dari tipe A dapat digunakan di mana B diperlukan, jika A <= B. Ini cukup banyak aksioma dalam arsitektur OO, yaitu. diasumsikan bahwa semua subclass akan memiliki properti ini (dan jika tidak, subtipe tersebut bermasalah dan perlu diperbaiki).

Namun, ternyata prinsip ini tidak realistis / tidak mewakili sebagian besar kode, atau memang tidak mungkin dipenuhi (dalam kasus yang tidak sepele)! Masalah ini, dikenal sebagai masalah persegi-persegi panjang atau masalah lingkaran-elips ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ) adalah salah satu contoh terkenal tentang betapa sulitnya untuk memenuhi.

Perhatikan bahwa kita dapat mengimplementasikan Kuadrat dan Persegi Panjang yang lebih banyak dan lebih setara secara ekivalen, tetapi hanya dengan membuang lebih banyak fungsionalitas sampai perbedaan tidak berguna.

Sebagai contoh, lihat http://okmij.org/ftp/Computation/Subtyping/

Warbo
sumber