Kelemahan hierarki kelas tradisional yang terkenal adalah bahwa mereka buruk ketika datang ke pemodelan dunia nyata. Sebagai contoh, berusaha mewakili spesies hewan dengan kelas. Sebenarnya ada beberapa masalah ketika melakukan itu, tetapi satu yang saya tidak pernah lihat solusinya adalah ketika sub-kelas "kehilangan" perilaku atau properti yang didefinisikan dalam kelas-super, seperti seekor penguin yang tidak bisa terbang (ada mungkin contoh yang lebih baik, tapi itu yang pertama muncul di benak saya).
Di satu sisi, Anda tidak ingin mendefinisikan, untuk setiap properti dan perilaku, beberapa bendera yang menentukan jika semuanya ada, dan memeriksanya setiap kali sebelum mengakses perilaku atau properti itu. Anda hanya ingin mengatakan bahwa burung dapat terbang, secara sederhana dan jelas, di kelas Burung. Tetapi kemudian akan lebih baik jika seseorang dapat mendefinisikan "pengecualian" sesudahnya, tanpa harus menggunakan beberapa peretasan yang mengerikan di mana-mana. Ini sering terjadi ketika suatu sistem telah produktif untuk sementara waktu. Anda tiba-tiba menemukan "pengecualian" yang sama sekali tidak sesuai dengan desain aslinya, dan Anda tidak ingin mengubah sebagian besar kode Anda untuk mengakomodasi itu.
Jadi, apakah ada beberapa bahasa atau pola desain yang dapat menangani masalah ini dengan bersih, tanpa memerlukan perubahan besar pada "super-class", dan semua kode yang menggunakannya? Bahkan jika suatu solusi hanya menangani kasus tertentu, beberapa solusi mungkin bersama-sama membentuk strategi yang lengkap.
Setelah berpikir lebih jauh, saya sadar saya lupa tentang Prinsip Pergantian Liskov. Itu sebabnya kamu tidak bisa melakukannya. Dengan asumsi Anda mendefinisikan "ciri / antarmuka" untuk semua "kelompok fitur" utama, Anda dapat dengan bebas menerapkan ciri-ciri di berbagai cabang hierarki, seperti sifat Terbang dapat diimplementasikan oleh Burung, dan beberapa jenis tupai dan ikan.
Jadi pertanyaan saya bisa berjumlah "Bagaimana saya bisa membatalkan penerapan suatu sifat?" Jika super-class Anda adalah Java Serializable, Anda harus menjadi salah satu juga, bahkan jika tidak ada cara bagi Anda untuk membuat serial keadaan Anda, misalnya jika Anda mengandung "Socket".
Salah satu cara untuk melakukannya adalah dengan selalu mendefinisikan semua sifat Anda berpasangan sejak awal: Terbang dan Tidak Menempati (yang akan melempar UnsupportedOperationException, jika tidak dicentang). Tidak-sifat tidak akan mendefinisikan antarmuka baru, dan dapat dengan mudah diperiksa. Kedengarannya seperti solusi "murah", khususnya jika digunakan sejak awal.
sumber
function save_yourself_from_crashing_airplane(Bird b) { f.fly() }
akan menjadi jauh lebih rumit. (seperti yang dikatakan Peter Török, itu melanggar LSP)" it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"
apakah Anda mempertimbangkan metode pabrik yang mengendalikan perilaku hacky?NotSupportedException
dariPenguin.fly()
.class Penguin < Bird; undef fly; end;
. Apakah Anda harus menjawab pertanyaan lain?Jawaban:
Seperti yang orang lain katakan Anda harus menentang LSP.
Namun, dapat dikatakan bahwa subkelas hanyalah perpanjangan arbiter dari kelas super. Itu adalah objek baru dengan haknya sendiri dan satu-satunya hubungan dengan kelas super adalah bahwa ia menggunakan fondasi.
Ini bisa masuk akal secara logis, daripada mengatakan Penguin adalah Burung. Pepatah Penguin Anda mewarisi beberapa bagian perilaku dari Bird.
Secara umum bahasa dinamis memungkinkan Anda untuk mengekspresikan ini dengan mudah, contoh menggunakan JavaScript berikut di bawah ini:
Dalam kasus khusus ini,
Penguin
aktif membayangiBird.fly
metode yang diwarisi dengan menulisfly
properti dengan nilaiundefined
pada objek.Sekarang Anda mungkin mengatakan itu
Penguin
tidak bisa diperlakukan seperti biasaBird
lagi. Tapi seperti yang disebutkan, di dunia nyata itu tidak bisa. Karena kami memodelkanBird
sebagai entitas terbang.Alternatifnya adalah tidak membuat asumsi luas bahwa Burung bisa terbang. Masuk akal untuk memiliki
Bird
abstraksi yang memungkinkan semua burung mewarisi darinya, tanpa kegagalan. Ini berarti hanya membuat asumsi bahwa semua subclass dapat memegang.Secara umum ide Mixin berlaku baik di sini. Memiliki kelas dasar yang sangat tipis, dan mencampur semua perilaku lain ke dalamnya.
Contoh:
Jika Anda penasaran, saya memiliki implementasi
Object.make
Tambahan:
Anda tidak "membatalkan penerapan" suatu sifat. Anda cukup memperbaiki hierarki warisan Anda. Entah Anda dapat memenuhi kontrak kelas super Anda atau Anda tidak boleh berpura-pura seperti itu.
Di sinilah komposisi objek bersinar.
Selain itu, Serializable tidak berarti semuanya harus diserialisasi, itu hanya berarti "menyatakan bahwa Anda peduli" harus diserialisasi.
Anda seharusnya tidak menggunakan sifat "NotX". Itu hanya kode mengasapi yang menghebohkan. Jika suatu fungsi mengharapkan objek terbang, itu harus crash dan terbakar ketika Anda memberikannya mammoth.
sumber
AFAIK semua bahasa berbasis warisan dibangun di atas Prinsip Substitusi Liskov . Menghapus / menonaktifkan properti kelas dasar di subkelas jelas akan melanggar LSP, jadi saya tidak berpikir kemungkinan seperti itu diterapkan di mana saja. Dunia nyata memang berantakan, dan tidak dapat secara tepat dimodelkan oleh abstraksi matematis.
Beberapa bahasa memberikan sifat atau campuran, tepatnya untuk mengatasi masalah tersebut dengan cara yang lebih fleksibel.
sumber
Class
adalah subclass dariModule
meskipunClass
IS-NOT-AModule
. Tetapi masih masuk akal untuk menjadi subclass, karena ia menggunakan kembali banyak kode. OTOH,StringIO
IS-AIO
, tetapi keduanya tidak memiliki hubungan pewarisan (tentu saja selain dari keduanyaObject
, tentu saja), karena mereka tidak membagikan kode apa pun. Kelas untuk berbagi kode, tipe untuk menggambarkan protokol.IO
danStringIO
memiliki protokol yang sama, oleh karena itu tipe yang sama, tetapi kelas mereka tidak berhubungan.Fly()
adalah dalam contoh pertama dalam: Pola Desain Kepala Pertama untuk Pola Strategi , dan ini adalah situasi yang baik mengapa Anda harus "Mendukung komposisi daripada warisan." .Anda dapat mencampur komposisi dan pewarisan dengan memiliki supertipe
FlyingBird
,FlightlessBird
yang memiliki perilaku yang benar disuntikkan oleh Pabrik, bahwa subtipe yang relevan misalnyaPenguin : FlightlessBird
dapatkan secara otomatis, dan hal lain yang benar-benar spesifik ditangani oleh Pabrik sebagai hal biasa.sumber
Bukankah masalah sebenarnya yang Anda asumsikan
Bird
memilikiFly
metode? Kenapa tidak:Sekarang masalah yang jelas adalah multiple inheritance (
Duck
), jadi yang Anda butuhkan adalah interface:sumber
Pertama, YES, bahasa apa pun yang memungkinkan modifikasi dinamis objek mudah akan memungkinkan Anda untuk melakukan itu. Di Ruby, misalnya, Anda dapat dengan mudah menghapus metode.
Tapi seperti yang dikatakan Péter Török, itu akan melanggar LSP .
Pada bagian ini, saya akan melupakan LSP, dan menganggap bahwa:
Kamu berkata :
Sepertinya yang Anda inginkan adalah Python " meminta pengampunan daripada izin "
Buat Penguin Anda melempar pengecualian atau mewarisi dari kelas NonFlyingBird yang melempar pengecualian (kode pseudo):
Ngomong-ngomong, apa pun yang Anda pilih: mengajukan pengecualian atau menghapus metode, pada akhirnya, kode berikut (seandainya bahasa Anda mendukung penghapusan metode):
akan melempar pengecualian runtime.
sumber
Seperti yang ditunjukkan seseorang di atas dalam komentar, penguin adalah burung, penguin tidak bisa terbang, tidak semua burung bisa terbang.
Jadi Bird.fly () seharusnya tidak ada atau diizinkan untuk tidak bekerja. Saya lebih suka yang pertama.
Memiliki FlyingBird extends Bird memiliki .fly () metode akan benar, tentu saja.
sumber
Masalah sebenarnya dengan contoh fly () adalah input dan output operasi tidak didefinisikan dengan benar. Apa yang dibutuhkan burung untuk terbang? Dan apa yang terjadi setelah penerbangan berhasil? Tipe parameter dan tipe pengembalian untuk fungsi fly () harus memiliki informasi itu. Kalau tidak, desain Anda tergantung pada efek samping acak dan apa pun bisa terjadi. Bagian apa pun yang menyebabkan seluruh masalah, antarmuka tidak didefinisikan dengan benar dan semua jenis implementasi diizinkan.
Jadi, alih-alih ini:
Anda harus memiliki sesuatu seperti ini:
Sekarang secara eksplisit mendefinisikan batas fungsionalitas - perilaku terbang Anda hanya memiliki satu float untuk memutuskan - jarak dari tanah, ketika diberi posisi. Sekarang seluruh masalah secara otomatis menyelesaikan sendiri. Seekor burung yang tidak bisa terbang hanya mengembalikan 0,0 dari fungsi itu, ia tidak pernah meninggalkan tanah. Itu adalah perilaku yang benar untuk itu, dan sekali satu float diputuskan, Anda tahu Anda telah sepenuhnya mengimplementasikan antarmuka.
Perilaku nyata bisa sulit untuk dikodekan ke tipe, tapi itu satu-satunya cara untuk menentukan antarmuka Anda dengan benar.
Sunting: Saya ingin memperjelas satu aspek. Fungsi float-> float versi fly () juga penting karena ia mendefinisikan sebuah path. Versi ini berarti bahwa satu burung tidak dapat menggandakan dirinya secara ajaib saat terbang. Inilah sebabnya mengapa parameternya adalah float tunggal - itu posisi di jalur yang diambil burung. Jika Anda ingin jalur yang lebih rumit, maka Point2d posinpath (float x); yang menggunakan fungsi x sama dengan fungsi fly ().
sumber
Secara teknis Anda dapat melakukan ini di hampir semua bahasa yang diketik dinamis / bebek (JavaScript, Ruby, Lua, dll.) Tetapi itu hampir selalu merupakan ide yang sangat buruk. Menghapus metode dari sebuah kelas adalah mimpi buruk pemeliharaan, mirip dengan menggunakan variabel global (mis. Anda tidak dapat mengatakan dalam satu modul bahwa keadaan global belum dimodifikasi di tempat lain).
Pola yang baik untuk masalah yang Anda gambarkan adalah Dekorator atau Strategi, merancang arsitektur komponen. Pada dasarnya, alih-alih menghapus perilaku yang tidak dibutuhkan dari sub-kelas, Anda membangun objek dengan menambahkan perilaku yang diperlukan. Jadi untuk membuat sebagian besar burung, Anda akan menambahkan komponen terbang, tetapi jangan menambahkan komponen itu ke penguin Anda.
sumber
Peter telah menyebutkan Prinsip Substitusi Liskov, tetapi saya merasa itu perlu dijelaskan.
Dengan demikian, jika Burung (objek x tipe T) dapat terbang (q (x)) maka Penguin (objek y dari tipe S) dapat terbang (q (y)), menurut definisi. Tapi jelas bukan itu masalahnya. Ada juga makhluk lain yang bisa terbang tetapi bukan tipe Burung.
Bagaimana Anda menangani ini tergantung pada bahasanya. Jika suatu bahasa mendukung banyak pewarisan maka Anda harus menggunakan kelas abstrak untuk makhluk yang dapat terbang; jika suatu bahasa lebih menyukai antarmuka maka itu adalah solusinya (dan implementasi fly harus dienkapsulasi daripada diwarisi); atau, jika bahasa mendukung Duck Typing (tidak ada permainan kata-kata yang dimaksudkan) maka Anda bisa menerapkan metode terbang pada kelas-kelas yang dapat dan menyebutnya jika ada.
Tetapi setiap properti dari superclass harus berlaku untuk semua subkelasnya.
[Sebagai tanggapan untuk mengedit]
Menerapkan "sifat" CanFly to Bird tidak lebih baik. Masih menyarankan kode panggilan agar semua burung bisa terbang.
Suatu sifat dalam istilah yang Anda tetapkan itu adalah persis apa yang dimaksud Liskov ketika dia mengatakan "properti".
sumber
Mari saya mulai dengan menyebutkan (seperti orang lain) Prinsip Pergantian Liskov, yang menjelaskan mengapa Anda tidak harus melakukan ini. Namun masalah yang harus Anda lakukan adalah desain. Dalam beberapa kasus mungkin tidak penting bahwa Penguin tidak dapat benar-benar terbang. Mungkin Anda bisa meminta Penguin melempar InsufficientWingsException ketika diminta untuk terbang, selama Anda jelas dalam dokumentasi Bird :: fly () yang mungkin melempar untuk burung yang tidak bisa terbang. Tentu ada tes untuk melihat apakah itu benar-benar bisa terbang, meskipun itu membengkak antarmuka.
Alternatifnya adalah merestrukturisasi kelas Anda. Mari kita buat kelas "FlyingCreature" (atau antarmuka yang lebih baik, jika Anda berurusan dengan bahasa yang memungkinkannya). "Bird" tidak mewarisi dari FlyingCreature, tetapi Anda dapat membuat "FlyingBird" yang membuatnya. Lark, Vulture, dan Eagle semuanya mewarisi dari FlyingBird. Penguin tidak. Itu hanya mewarisi dari Bird.
Ini sedikit lebih rumit daripada struktur yang naif, tetapi memiliki keuntungan menjadi akurat. Anda akan mencatat bahwa semua kelas yang diharapkan ada di sana (Burung) dan pengguna biasanya dapat mengabaikan yang 'diciptakan' (FlyingCreature) jika tidak penting apakah makhluk Anda dapat terbang atau tidak.
sumber
Cara khas untuk menangani situasi seperti ini adalah dengan melempar sesuatu seperti
UnsupportedOperationException
(Java) resp.NotImplementedException
(C #).sumber
Banyak jawaban bagus dengan banyak komentar, tetapi mereka semua tidak setuju, dan saya hanya bisa memilih satu, jadi saya akan meringkas semua pandangan yang saya setujui di sini.
0) Jangan anggap "mengetik statis" (saya lakukan ketika saya bertanya, karena saya melakukan Java hampir secara eksklusif). Pada dasarnya, masalahnya sangat tergantung pada jenis bahasa yang digunakan.
1) Seseorang harus memisahkan jenis-hierarki dari hirarki penggunaan kembali kode dalam desain dan di kepala seseorang, bahkan jika mereka sebagian besar tumpang tindih. Secara umum, gunakan kelas untuk digunakan kembali, dan antarmuka untuk tipe.
2) Alasan mengapa biasanya Bird IS-A Fly adalah karena kebanyakan burung dapat terbang, sehingga praktis dari sudut pandang penggunaan kembali kode, tetapi mengatakan bahwa Bird IS-A Fly sebenarnya salah karena setidaknya ada satu pengecualian (Pinguin).
3) Dalam bahasa statis dan dinamis, Anda bisa melempar pengecualian. Tetapi ini hanya dapat digunakan jika secara eksplisit dinyatakan dalam "kontrak" dari kelas / antarmuka yang menyatakan fungsionalitas, jika tidak maka itu adalah "pelanggaran kontrak". Ini juga berarti bahwa Anda sekarang harus siap untuk menangkap pengecualian di mana-mana, sehingga Anda menulis lebih banyak kode di situs panggilan, dan itu adalah kode yang jelek.
4) Dalam beberapa bahasa dinamis, sebenarnya mungkin untuk "menghapus / menyembunyikan" fungsi kelas-super. Jika memeriksa keberadaan fungsi adalah bagaimana Anda memeriksa "IS-A" dalam bahasa itu, maka ini adalah solusi yang memadai dan masuk akal. Jika di sisi lain, operasi "IS-A" adalah sesuatu yang lain yang masih mengatakan objek Anda "harus" mengimplementasikan fungsionalitas yang sekarang hilang, maka kode panggilan Anda akan menganggap bahwa fungsionalitas itu ada dan memanggilnya dan crash, jadi itu semacam jumlah pengecualian.
5) Alternatif yang lebih baik adalah dengan benar-benar memisahkan sifat Terbang dari sifat Burung. Jadi burung terbang harus secara eksplisit memperluas / mengimplementasikan Burung dan Terbang / Terbang. Ini mungkin desain yang paling bersih, karena Anda tidak perlu "menghapus" apa pun. Satu kelemahannya adalah sekarang hampir setiap burung harus mengimplementasikan Bird dan Fly, jadi Anda menulis lebih banyak kode. Cara mengatasi hal ini adalah dengan memiliki kelas perantara FlyingBird, yang mengimplementasikan Bird dan Fly, dan mewakili kasus umum, tetapi penyelesaian ini mungkin penggunaan terbatas tanpa pewarisan berganda.
6) Alternatif lain yang tidak membutuhkan pewarisan berganda adalah menggunakan komposisi alih-alih pewarisan. Setiap aspek hewan dimodelkan oleh kelas independen, dan Burung konkret adalah komposisi Burung, dan mungkin Terbang atau Berenang, ... Anda mendapatkan penggunaan kembali kode penuh, tetapi harus melakukan satu atau beberapa langkah tambahan untuk mendapatkan fungsionalitas Terbang, ketika Anda memiliki referensi Burung konkret. Juga, bahasa alami "objek IS-A Fly" dan "objek AS-A (cast) Fly" tidak akan berfungsi lagi, jadi Anda harus menemukan sintaksis Anda sendiri (beberapa bahasa dinamis mungkin memiliki cara untuk mengatasi hal ini). Ini mungkin membuat kode Anda lebih rumit.
7) Tentukan sifat Terbang Anda sehingga menawarkan jalan keluar yang jelas untuk sesuatu yang tidak bisa terbang. Fly.getNumberOfWings () dapat mengembalikan 0. Jika Fly.fly (arah, currentPotinion) harus mengembalikan posisi baru setelah penerbangan, maka Penguin.fly () dapat mengembalikan posisi saat ini tanpa mengubahnya. Anda mungkin berakhir dengan kode yang secara teknis berfungsi, tetapi ada beberapa peringatan. Pertama, beberapa kode mungkin tidak memiliki perilaku "tidak melakukan apa pun" yang jelas. Juga, jika seseorang memanggil x.fly (), mereka akan mengharapkannya melakukan sesuatu , bahkan jika komentar mengatakan fly () tidak boleh melakukan apa-apa . Akhirnya, penguin IS-A Flying akan tetap benar, yang mungkin membingungkan bagi programmer.
8) Lakukan sebagai 5), tetapi gunakan komposisi untuk berkeliling kasus yang akan membutuhkan banyak pewarisan. Ini adalah opsi yang saya lebih suka untuk bahasa statis, karena 6) tampaknya lebih rumit (dan mungkin memerlukan lebih banyak memori karena kita memiliki lebih banyak objek). Bahasa yang dinamis mungkin membuat 6) kurang rumit, tapi saya ragu itu akan menjadi kurang rumit dari 5).
sumber
Menentukan perilaku default (tandai sebagai virtual) di kelas dasar dan menimpanya sebagai keharusan. Dengan begitu setiap burung bisa "terbang".
Bahkan penguin terbang, meluncur di es di ketinggian nol!
Perilaku terbang dapat ditimpa jika perlu.
Kemungkinan lain adalah memiliki Antarmuka Terbang. Tidak semua burung akan mengimplementasikan antarmuka itu.
Properti tidak dapat dihapus, jadi itu sebabnya penting untuk mengetahui properti apa yang umum di semua burung. Saya pikir itu lebih merupakan masalah desain untuk memastikan properti umum diimplementasikan pada tingkat dasar.
sumber
Saya pikir pola yang Anda cari adalah polimorfisme lama yang bagus. Meskipun Anda mungkin dapat menghapus antarmuka dari kelas dalam beberapa bahasa, itu mungkin bukan ide yang baik karena alasan yang diberikan oleh Péter Török. Namun, dalam bahasa OO apa pun, Anda dapat mengganti metode untuk mengubah perilakunya, dan itu termasuk tidak melakukan apa-apa. Untuk meminjam contoh Anda, Anda dapat memberikan metode Penguin :: fly () yang melakukan salah satu dari yang berikut:
Properti bisa sedikit lebih mudah untuk ditambahkan dan dihapus jika Anda berencana ke depan. Anda bisa menyimpan properti dalam array peta / kamus / asosiatif alih-alih menggunakan variabel instan. Anda dapat menggunakan pola Pabrik untuk menghasilkan contoh standar dari struktur tersebut, sehingga Burung yang datang dari BirdFactory akan selalu memulai dengan set properti yang sama. Kode Nilai Kunci Objective-C adalah contoh yang bagus untuk hal semacam ini.
Catatan: Pelajaran serius dari komentar di bawah ini adalah bahwa sementara mengganti perilaku bisa berhasil, itu tidak selalu merupakan solusi terbaik. Jika Anda merasa perlu melakukan ini dengan cara yang signifikan, Anda harus mempertimbangkan bahwa sinyal kuat bahwa grafik warisan Anda cacat. Tidak selalu mungkin untuk memperbaiki kelas yang Anda warisi, tetapi ketika itu yang sering solusi yang lebih baik.
Menggunakan contoh Penguin Anda, salah satu cara untuk refactor adalah dengan memisahkan kemampuan terbang dari kelas Burung. Karena tidak semua burung bisa terbang, termasuk metode lalat () di Bird tidak pantas dan mengarah langsung ke masalah yang Anda tanyakan. Jadi, pindahkan metode fly () (dan mungkin lepas landas () dan land ()) ke kelas atau antarmuka Aviator (tergantung bahasa). Ini memungkinkan Anda membuat kelas FlyingBird yang mewarisi dari Bird dan Aviator (atau mewarisi dari Bird dan mengimplementasikan Aviator). Penguin dapat terus mewarisi langsung dari Bird tetapi bukan Aviator, sehingga menghindari masalah. Pengaturan seperti itu mungkin juga memudahkan untuk membuat kelas untuk hal-hal terbang lainnya: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect, dan sebagainya.
sumber