Apakah ada bahasa atau pola desain yang memungkinkan * penghapusan * perilaku objek atau properti dalam hierarki kelas?

28

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.

Sebastien Diot
sumber
3
'tanpa harus menggunakan beberapa peretasan yang mengerikan di mana-mana': menonaktifkan perilaku IS peretasan yang mengerikan: itu menyiratkan yang 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)
keppla
Kombinasi dari pola Strategi dan warisan mungkin memungkinkan Anda untuk "menyusun" perilaku warisan untuk tipe super tertentu? Ketika Anda mengatakan: " 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?
StuperUser
1
Seseorang tentu saja bisa melempar NotSupportedExceptiondari Penguin.fly().
Felix Dombek
Sejauh bahasa berjalan, Anda tentu saja dapat membatalkan penerapan metode dalam kelas anak. Misalnya, di Ruby: class Penguin < Bird; undef fly; end;. Apakah Anda harus menjawab pertanyaan lain?
Nathan Long
Ini akan melanggar prinsip liskov dan bisa dibilang seluruh titik OOP.
deadalnix

Jawaban:

17

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:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

Dalam kasus khusus ini, Penguinaktif membayangi Bird.flymetode yang diwarisi dengan menulis flyproperti dengan nilai undefinedpada objek.

Sekarang Anda mungkin mengatakan itu Penguintidak bisa diperlakukan seperti biasa Birdlagi. Tapi seperti yang disebutkan, di dunia nyata itu tidak bisa. Karena kami memodelkan Birdsebagai entitas terbang.

Alternatifnya adalah tidak membuat asumsi luas bahwa Burung bisa terbang. Masuk akal untuk memiliki Birdabstraksi 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:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Jika Anda penasaran, saya memiliki implementasiObject.make

Tambahan:

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

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.

Raynos
sumber
10
"Di dunia nyata itu tidak bisa." Ya bisa. Seekor penguin adalah burung. Kemampuan untuk terbang bukan milik burung, itu hanyalah sifat kebetulan dari sebagian besar spesies burung. Properti yang mendefinisikan burung adalah "hewan berbulu, bersayap, bipedal, endotermik, bertelur, dan bertulang belakang" (Wikipedia) - tidak ada yang bisa terbang ke sana.
pdr
2
@ pdr lagi itu tergantung pada definisi Anda tentang burung. Karena saya menggunakan istilah "Burung" yang saya maksud adalah abstraksi kelas yang kami gunakan untuk mewakili burung termasuk metode terbang. Saya juga menyebutkan bahwa Anda dapat membuat abstraksi kelas Anda kurang spesifik. Juga seekor penguin tidak berbulu.
Raynos
2
@ Raynos: Penguin memang berbulu. Bulu mereka cukup pendek dan padat, tentu saja.
Jon Purdy
@ JonPurdy cukup adil, saya selalu membayangkan mereka punya bulu.
Raynos
+1 secara umum, dan khususnya untuk "mammoth". LOL!
Sebastien Diot
28

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.

Péter Török
sumber
1
LSP adalah untuk tipe , bukan untuk kelas .
Jörg W Mittag
2
@ PéterTörök: Pertanyaan ini tidak akan ada jika tidak :-) Saya dapat memikirkan dua contoh dari Ruby. Classadalah subclass dari Modulemeskipun ClassIS-NOT-A Module. Tetapi masih masuk akal untuk menjadi subclass, karena ia menggunakan kembali banyak kode. OTOH, StringIOIS-A IO, tetapi keduanya tidak memiliki hubungan pewarisan (tentu saja selain dari keduanya Object, tentu saja), karena mereka tidak membagikan kode apa pun. Kelas untuk berbagi kode, tipe untuk menggambarkan protokol. IOdan StringIOmemiliki protokol yang sama, oleh karena itu tipe yang sama, tetapi kelas mereka tidak berhubungan.
Jörg W Mittag
1
@ JörgWMittag, OK, sekarang saya mengerti lebih baik apa yang Anda maksud. Namun, bagi saya contoh pertama Anda terdengar lebih seperti penyalahgunaan warisan daripada ekspresi dari beberapa masalah mendasar yang tampaknya Anda sarankan. IMO warisan publik tidak boleh digunakan untuk menggunakan kembali implementasi, hanya untuk mengekspresikan hubungan subtipe (is-a). Dan fakta bahwa itu dapat disalahgunakan tidak mendiskualifikasi itu - saya tidak bisa membayangkan alat yang dapat digunakan dari domain apa pun yang tidak dapat disalahgunakan.
Péter Török
2
Untuk orang-orang yang mengangkat jawaban ini: perhatikan bahwa ini tidak benar-benar menjawab pertanyaan, terutama setelah klarifikasi yang diedit. Saya pikir jawaban ini tidak pantas dijatuhkan, karena apa yang dia katakan sangat benar dan penting untuk diketahui, tetapi dia tidak benar-benar menjawab pertanyaan itu.
jhocking
1
Bayangkan sebuah Java di mana hanya antarmuka adalah tipe, kelas tidak, dan subclass dapat "un-implement" antarmuka dari superclass mereka, dan Anda punya ide kasar, saya pikir.
Jörg W Mittag
15

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, FlightlessBirdyang memiliki perilaku yang benar disuntikkan oleh Pabrik, bahwa subtipe yang relevan misalnya Penguin : FlightlessBirddapatkan secara otomatis, dan hal lain yang benar-benar spesifik ditangani oleh Pabrik sebagai hal biasa.

StuperUser
sumber
1
Saya menyebutkan pola Dekorator dalam jawaban saya, tetapi pola Strategi juga bekerja dengan baik.
jhocking
1
+1 untuk "Komposisi kesukaan daripada warisan." Namun, perlunya pola desain khusus untuk menerapkan komposisi dalam bahasa yang diketik secara statis memperkuat bias saya terhadap bahasa dinamis seperti Ruby.
Roy Tinker
11

Bukankah masalah sebenarnya yang Anda asumsikan Birdmemiliki Flymetode? Kenapa tidak:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Sekarang masalah yang jelas adalah multiple inheritance ( Duck), jadi yang Anda butuhkan adalah interface:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}
Scott Whitlock
sumber
3
Masalahnya adalah bahwa evolusi tidak mengikuti Prinsip Pergantian Liskov, dan melakukan pewarisan dengan penghapusan fitur.
Donal Fellows
7

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:

  • Bird adalah kelas dengan metode terbang ()
  • Penguin harus mewarisi dari Burung
  • Penguin tidak bisa terbang ()
  • Saya tidak peduli apakah itu desain yang baik atau cocok dengan dunia nyata, karena itu adalah contoh yang diberikan dalam pertanyaan ini.

Kamu berkata :

Di satu sisi, Anda tidak ingin mendefinisikan untuk setiap properti dan perilaku beberapa flag yang menentukan jika semuanya ada, dan memeriksanya setiap kali sebelum mengakses perilaku atau properti itu

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):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

Ngomong-ngomong, apa pun yang Anda pilih: mengajukan pengecualian atau menghapus metode, pada akhirnya, kode berikut (seandainya bahasa Anda mendukung penghapusan metode):

var bird:Bird = new Penguin();
bird.fly();

akan melempar pengecualian runtime.

David
sumber
"Buat saja Penguin Anda melempar pengecualian atau mewarisi dari kelas NonFlyingBird yang melempar pengecualian" Itu masih merupakan pelanggaran LSP. Ia masih menyarankan agar seekor Penguin bisa terbang, meskipun penerapan lalatnya gagal. Seharusnya tidak pernah ada metode terbang pada Penguin.
pdr
@ pdr: tidak menyarankan Penguin bisa terbang, tetapi harus terbang (itu kontrak). Pengecualian akan memberi tahu Anda bahwa itu tidak bisa . Ngomong-ngomong, saya tidak mengklaim itu adalah praktik OOP yang baik, saya hanya memberikan jawaban untuk sebagian dari pertanyaan
David
Intinya adalah bahwa Penguin seharusnya tidak diharapkan terbang hanya karena itu adalah Burung. Jika saya ingin menulis kode yang mengatakan "Jika x bisa terbang, lakukan ini; kalau tidak, lakukanlah." Saya harus menggunakan try / catch dalam versi Anda, di mana saya seharusnya hanya dapat menanyakan objek apakah ia dapat terbang (ada casting atau metode pengecekan). Mungkin hanya di kata-kata, tetapi jawaban Anda menyiratkan bahwa melempar pengecualian sesuai dengan LSP.
pdr
@ pdr "Saya harus menggunakan coba / tangkap dalam versi Anda" -> itulah inti dari meminta pengampunan daripada izin (karena bahkan Bebek bisa mematahkan sayapnya dan tidak dapat terbang). Saya akan memperbaiki kata-katanya.
David
"Itulah inti dari meminta maaf daripada izin." Ya, kecuali bahwa itu memungkinkan kerangka kerja untuk melemparkan jenis pengecualian yang sama untuk setiap metode yang hilang, jadi Python "coba: kecuali AttributeError:" persis sama dengan C # "jika" (X adalah Y) {} else {} "dan langsung dikenali Dengan demikian. Tetapi jika Anda dengan sengaja melempar CannotFlyException untuk mengesampingkan fungsi default fly () di Bird maka itu menjadi kurang dapat dikenali.
pdr
7

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.

alex
sumber
Saya setuju, Fly harus menjadi antarmuka yang dapat diterapkan burung . Itu bisa diimplementasikan sebagai metode juga dengan perilaku default yang bisa diganti, tetapi pendekatan yang lebih bersih menggunakan antarmuka.
Jon Raynor
6

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:

class Bird {
public:
   virtual void fly()=0;
};

Anda harus memiliki sesuatu seperti ini:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

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 ().

tp1
sumber
1
Saya cukup suka jawaban Anda. Saya pikir itu pantas mendapat lebih banyak suara.
Sebastien Diot
2
Jawaban yang sangat bagus. Masalahnya adalah bahwa pertanyaan hanya melambaikan tangannya untuk apa yang terbang () sebenarnya. Setiap implementasi nyata dari lalat akan memiliki, paling tidak, tujuan - lalat (koordinat tujuan) yang dalam kasus penguin, dapat diganti untuk mengimplementasikan {return currentPosition)}
Chris Cudmore
4

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.

jhocking
sumber
3

Peter telah menyebutkan Prinsip Substitusi Liskov, tetapi saya merasa itu perlu dijelaskan.

Biarkan q (x) menjadi properti yang dapat dibuktikan tentang objek x tipe T. Kemudian q (y) harus dapat dibuktikan untuk objek y dari tipe S di mana S adalah subtipe dari T.

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

pdr
sumber
2

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.

DJClayworth
sumber
0

Cara khas untuk menangani situasi seperti ini adalah dengan melempar sesuatu seperti UnsupportedOperationException(Java) resp. NotImplementedException(C #).

pengguna281377
sumber
Selama Anda mendokumentasikan kemungkinan ini di Bird.
DJClayworth
0

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

Sebastien Diot
sumber
0

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.

class eagle : bird, IFly
class penguin : bird

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.

Jon Raynor
sumber
-1

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:

  • tidak ada
  • melempar pengecualian
  • memanggil metode Penguin :: swim () sebagai gantinya
  • menegaskan bahwa penguin di bawah air (mereka semacam "terbang" melalui air)

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.

Caleb
sumber
2
-1 untuk bahkan menyarankan memanggil Penguin :: swim (). Itu melanggar prinsip tercengang dan akan menyebabkan programmer pemeliharaan di mana pun mengutuk nama Anda.
DJClayworth
1
@DJClayworth Seperti contohnya ada di sisi konyol di tempat pertama, downvoting untuk pelanggaran perilaku terbang () dan berenang () tampaknya agak banyak. Tetapi jika Anda benar-benar ingin melihat ini dengan serius, saya akan setuju bahwa lebih mungkin Anda pergi ke arah lain dan menerapkan berenang () dalam hal terbang (). Bebek berenang dengan mengayuh kaki mereka; penguin berenang dengan mengepakkan sayapnya.
Caleb
1
Saya setuju pertanyaan itu konyol, tetapi masalahnya adalah saya telah melihat orang melakukan ini dalam kehidupan nyata - menggunakan panggilan yang ada yang "tidak benar-benar melakukan apa pun" untuk menerapkan fungsi yang langka. Ini benar-benar mengacaukan kode, dan biasanya berakhir dengan harus menulis "if (! (MyBird instanceof Penguin)) fly ();" di banyak tempat, sambil berharap tidak ada yang menciptakan kelas burung unta.
DJClayworth
Pernyataan itu bahkan lebih buruk. Jika saya memiliki array Burung, yang semuanya memiliki metode fly (), saya tidak ingin kegagalan pernyataan ketika saya memanggil fly () pada mereka.
DJClayworth
1
Saya tidak membaca dokumentasi Penguin , karena saya diberi array Burung dan saya tidak tahu Penguin akan berada di array. Saya memang membaca dokumentasi Burung yang mengatakan bahwa ketika saya sebut terbang () burung itu terbang. Jika dokumentasi itu dengan jelas menyatakan bahwa pengecualian mungkin dilemparkan jika burung itu tidak dapat terbang, saya akan mengizinkannya. Jika dikatakan bahwa memanggil lalat () kadang-kadang membuatnya berenang, saya akan berubah menggunakan perpustakaan kelas yang berbeda. Atau pergi untuk minum yang sangat besar.
DJClayworth