Warisan vs Agregasi [ditutup]

151

Ada dua aliran pemikiran tentang cara terbaik untuk memperluas, meningkatkan, dan menggunakan kembali kode dalam sistem berorientasi objek:

  1. Warisan: memperluas fungsi kelas dengan membuat subclass. Timpa anggota superclass di subclass untuk memberikan fungsionalitas baru. Buat metode abstrak / virtual untuk memaksa subclass untuk "mengisi-kosong" ketika superclass menginginkan antarmuka tertentu tetapi agnostik tentang implementasinya.

  2. Agregasi: membuat fungsionalitas baru dengan mengambil kelas lain dan menggabungkannya ke kelas baru. Pasang antarmuka umum ke kelas baru ini untuk interoperabilitas dengan kode lain.

Apa manfaat, biaya, dan konsekuensi dari masing-masing? Apakah ada alternatif lain?

Saya melihat debat ini muncul secara teratur, tapi saya tidak berpikir itu sudah ditanyakan pada Stack Overflow (meskipun ada beberapa diskusi terkait). Ada juga kurangnya hasil Google yang bagus untuk itu.

Craig Walker
sumber
9
Pertanyaan yang bagus, sayangnya saya tidak punya cukup waktu sekarang.
Toon Krijthe
4
Sebuah jawaban yang baik lebih baik daripada yang lebih cepat ... Saya akan menonton pertanyaan saya sendiri jadi saya akan memilih Anda setidaknya :-P
Craig Walker

Jawaban:

181

Ini bukan masalah mana yang terbaik, tetapi kapan harus menggunakan apa.

Dalam kasus-kasus 'normal', pertanyaan sederhana sudah cukup untuk mengetahui apakah kita membutuhkan warisan atau agregasi.

  • Jika Kelas baru adalah kurang lebih sebagai kelas yang asli. Gunakan warisan. Kelas baru sekarang menjadi subclass dari kelas asli.
  • Jika kelas baru harus memiliki kelas asli. Gunakan agregasi. Kelas baru sekarang memiliki kelas asli sebagai anggota.

Namun, ada area abu-abu besar. Jadi kita perlu beberapa trik lain.

  • Jika kami telah menggunakan warisan (atau kami berencana untuk menggunakannya) tetapi kami hanya menggunakan bagian dari antarmuka, atau kami terpaksa menimpa banyak fungsi untuk menjaga korelasi logis. Lalu kami memiliki bau tidak enak yang menandakan bahwa kami harus menggunakan agregasi.
  • Jika kami telah menggunakan agregasi (atau kami berencana untuk menggunakannya) tetapi kami tahu kami perlu menyalin hampir semua fungsionalitas. Lalu kita memiliki bau yang menunjuk ke arah warisan.

Untuk memotongnya. Kita harus menggunakan agregasi jika bagian dari antarmuka tidak digunakan atau harus diubah untuk menghindari situasi yang tidak masuk akal. Kita hanya perlu menggunakan warisan, jika kita membutuhkan hampir semua fungsi tanpa perubahan besar. Dan jika ragu, gunakan Agregasi.

Kemungkinan lain untuk, kasus bahwa kita memiliki kelas yang membutuhkan bagian dari fungsionalitas kelas asli, adalah untuk membagi kelas asli menjadi kelas root dan sub kelas. Dan biarkan kelas baru mewarisi dari kelas root. Tetapi Anda harus berhati-hati dengan ini, bukan untuk menciptakan pemisahan yang tidak logis.

Mari kita tambahkan contoh. Kami memiliki kelas 'Dog' dengan metode: 'Eat', 'Walk', 'Bark', 'Play'.

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Kita sekarang membutuhkan kelas 'Kucing', yang membutuhkan 'Makan', 'Jalan', 'Purr', dan 'Mainkan'. Jadi pertama-tama cobalah untuk memperpanjang dari Anjing.

class Cat is Dog
  Purr; 
end;

Terlihat, baiklah, tapi tunggu. Kucing ini bisa menggonggong (Pecinta Kucing akan membunuhku untuk itu). Dan seekor kucing menggonggong melanggar prinsip-prinsip alam semesta. Jadi kita perlu mengganti metode Bark agar tidak melakukan apa-apa.

class Cat is Dog
  Purr; 
  Bark = null;
end;

Ok, ini berhasil, tapi baunya buruk. Jadi mari kita coba agregasi:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

Ok, ini bagus. Kucing ini tidak menggonggong lagi, bahkan tidak diam. Tetapi masih memiliki anjing internal yang ingin keluar. Jadi mari kita coba solusi nomor tiga:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

Ini jauh lebih bersih. Tidak ada anjing internal. Dan kucing dan anjing berada pada level yang sama. Kami bahkan dapat memperkenalkan hewan peliharaan lain untuk memperluas model. Kecuali kalau itu ikan, atau sesuatu yang tidak bisa berjalan. Dalam hal ini kita perlu refactor lagi. Tetapi itu adalah sesuatu untuk waktu yang lain.

Toon Krijthe
sumber
5
"Menggunakan kembali hampir semua fungsi suatu kelas" adalah saat saya benar-benar lebih memilih warisan. Apa yang benar - benar saya sukai adalah bahasa yang memiliki kemampuan untuk dengan mudah mengatakan "mendelegasikan objek agregat ini untuk metode khusus ini"; itulah yang terbaik dari kedua dunia.
Craig Walker
Saya tidak yakin tentang bahasa lain, tetapi Delphi memiliki mekanisme, yang memungkinkan anggota mengimplementasikan bagian dari antarmuka.
Toon Krijthe
2
Tujuan C menggunakan Protokol yang memungkinkan Anda memutuskan apakah fungsi Anda diperlukan atau opsional.
cantfindaname88
1
Jawaban yang bagus dengan contoh praktis. Saya akan merancang kelas Pet dengan metode yang disebut makeSounddan membiarkan setiap subclass mengimplementasikan jenis suara mereka sendiri. Ini kemudian akan membantu dalam situasi di mana Anda memiliki banyak hewan peliharaan dan lakukan for_each pet in the list of pets, pet.makeSound.
blongho
38

Pada awal GOF mereka menyatakan

Komposisi objek yang disukai daripada warisan kelas.

Ini dibahas lebih lanjut di sini

toolkit
sumber
27

Perbedaannya biasanya dinyatakan sebagai perbedaan antara "adalah" dan "memiliki". Warisan, hubungan "adalah", dirangkum dengan baik dalam Prinsip Substitusi Liskov . Agregasi, hubungan "memiliki", hanya itu - itu menunjukkan bahwa objek agregasi memiliki salah satu objek agregat.

Perbedaan lebih lanjut juga ada - warisan pribadi dalam C ++ menunjukkan hubungan "diterapkan dalam hal", yang juga dapat dimodelkan dengan agregasi objek anggota (yang tidak terpapar) juga.

Harper Shelby
sumber
Juga, perbedaan dirangkum dengan baik dalam pertanyaan yang seharusnya Anda jawab. Saya ingin orang-orang pertama kali membaca pertanyaan sebelum mulai menerjemahkan. Apakah Anda secara buta mempromosikan pola desain untuk menjadi anggota klub elit?
Val
Saya tidak suka pendekatan ini, ini lebih tentang komposisi
Alireza Rahmani Khalili
15

Inilah argumen saya yang paling umum:

Dalam sistem berorientasi objek apa pun, ada dua bagian untuk kelas apa pun:

  1. Its antarmuka : "wajah publik" dari objek. Ini adalah seperangkat kemampuan yang diumumkannya ke seluruh dunia. Dalam banyak bahasa, himpunan didefinisikan dengan baik menjadi "kelas". Biasanya ini adalah tanda tangan metode objek, meskipun sedikit bervariasi berdasarkan bahasa.

  2. Its implementasi : "di belakang layar" pekerjaan yang objek tidak memuaskan antarmuka dan menyediakan fungsionalitas. Ini biasanya kode dan data anggota objek.

Salah satu prinsip dasar dari OOP adalah bahwa implementasinya dirangkum (yaitu: tersembunyi) di dalam kelas; satu-satunya hal yang harus dilihat orang luar adalah antarmuka.

Ketika subclass mewarisi dari subclass, biasanya mewarisi baik pelaksanaan dan antarmuka. Ini, pada gilirannya, berarti Anda dipaksa untuk menerima keduanya sebagai kendala pada kelas Anda.

Dengan agregasi, Anda bisa memilih implementasi atau antarmuka, atau keduanya - tetapi Anda tidak dipaksa untuk melakukannya. Fungsi suatu objek diserahkan kepada objek itu sendiri. Itu dapat tunduk pada objek lain seperti yang diinginkan, tetapi pada akhirnya bertanggung jawab untuk dirinya sendiri. Dalam pengalaman saya, ini mengarah ke sistem yang lebih fleksibel: yang lebih mudah untuk dimodifikasi.

Jadi, setiap kali saya mengembangkan perangkat lunak berorientasi objek, saya hampir selalu lebih suka agregasi daripada warisan.

Craig Walker
sumber
Saya akan berpikir bahwa Anda menggunakan kelas abstrak untuk mendefinisikan antarmuka dan kemudian menggunakan warisan langsung untuk setiap kelas implementasi konkret (membuatnya cukup dangkal). Setiap agregasi berada di bawah implementasi konkret.
orcmid
Jika Anda membutuhkan antarmuka tambahan dari itu, kembalikan melalui metode pada antarmuka antarmuka abstrak tingkat utama, bilas berulang.
orcmid
Apakah Anda pikir, agregasi lebih baik dalam skenario ini? Sebuah buku adalah SellingItem, DigitalDisc adalah codereview.stackexchange.com/questions/14077/… SelliingItem
LCJ
11

Saya memberi jawaban untuk "Is a" vs "Has a": mana yang lebih baik? .

Pada dasarnya saya setuju dengan orang lain: gunakan pewarisan hanya jika kelas turunan Anda benar - benar tipe yang Anda perluas, bukan hanya karena berisi data yang sama. Ingatlah bahwa pewarisan berarti subkelas memperoleh metode dan juga data.

Apakah masuk akal bagi kelas turunan Anda untuk memiliki semua metode superclass? Atau apakah Anda hanya diam-diam berjanji pada diri sendiri bahwa metode itu harus diabaikan di kelas turunan? Atau apakah Anda menemukan diri Anda mengganti metode dari superclass, menjadikannya no-ops sehingga tidak ada yang menyebut mereka secara tidak sengaja? Atau memberikan petunjuk ke alat pembuatan dokumen API Anda untuk menghilangkan metode dari dokumen?

Itu adalah petunjuk kuat bahwa agregasi adalah pilihan yang lebih baik dalam kasus itu.

Bill Karwin
sumber
6

Saya melihat banyak tanggapan "is-a vs has-a; mereka berbeda secara konseptual" tentang ini dan pertanyaan terkait.

Satu hal yang saya temukan dalam pengalaman saya adalah mencoba menentukan apakah suatu hubungan "is-a" atau "has-a" pasti gagal. Bahkan jika Anda dapat menentukan dengan tepat objek tersebut sekarang, mengubah persyaratan berarti bahwa Anda mungkin akan salah di beberapa titik di masa depan.

Hal lain yang saya temukan adalah bahwa sangat sulit untuk mengkonversi dari pewarisan ke agregasi begitu ada banyak kode yang ditulis di sekitar hierarki warisan. Hanya beralih dari superclass ke antarmuka berarti mengubah hampir setiap subkelas dalam sistem.

Dan, seperti yang saya sebutkan di bagian lain dalam posting ini, agregasi cenderung kurang fleksibel daripada warisan.

Jadi, Anda memiliki badai argumen yang sempurna terhadap warisan setiap kali Anda harus memilih satu atau yang lain:

  1. Pilihan Anda kemungkinan akan menjadi pilihan yang salah di beberapa titik
  2. Mengubah pilihan itu sulit setelah Anda membuatnya.
  3. Warisan cenderung menjadi pilihan yang lebih buruk karena lebih membatasi.

Jadi, saya cenderung memilih agregasi - bahkan ketika tampaknya ada hubungan is-a yang kuat.

Craig Walker
sumber
Persyaratan yang berkembang berarti desain OO Anda pasti akan salah. Warisan vs agregasi hanyalah puncak dari gunung es itu. Anda tidak dapat membuat arsitek untuk semua masa depan yang mungkin, jadi pergilah dengan ide XP: selesaikan persyaratan hari ini dan terima bahwa Anda mungkin harus melakukan refactor.
Bill Karwin
Saya suka pertanyaan dan pertanyaan ini. Khawatir tentang blurrung adalah-a, memiliki-a, dan menggunakan-a sekalipun. Saya mulai dengan antarmuka (bersama dengan mengidentifikasi abstraksi yang diimplementasikan saya ingin antarmuka). Tingkat tipuan tambahan sangat berharga. Jangan ikuti kesimpulan agregasi Anda.
orcmid
Tapi itu maksud saya. Baik warisan maupun agregasi adalah metode untuk menyelesaikan masalah. Salah satu metode itu menimbulkan semua jenis hukuman ketika Anda harus menyelesaikan masalah besok.
Craig Walker
Dalam pikiran saya, agregasi + antarmuka adalah alternatif untuk pewarisan / subkelas; Anda tidak dapat melakukan polimorfisme berdasarkan agregasi saja.
Craig Walker
3

Pertanyaannya biasanya diutarakan sebagai Komposisi vs. Warisan , dan telah diajukan di sini sebelumnya.

Bill the Lizard
sumber
Benar Anda .. lucu bahwa SO tidak memberikan itu sebagai salah satu pertanyaan terkait.
Craig Walker
2
Pencarian SO masih menyebalkan
Vinko Vrsalovic
Sepakat. Itu sebabnya saya tidak menutup ini. Itu, dan banyak orang sudah menjawab di sini.
Bill the Lizard
1
SO membutuhkan fitur untuk menggulung 2 pertanyaan (dan tanggapannya) menjadi 1
Craig Walker
3

Saya ingin berkomentar tentang pertanyaan awal, tetapi 300 karakter menggigit [; <).

Saya pikir kita perlu berhati-hati. Pertama, ada lebih banyak rasa daripada dua contoh agak spesifik yang dibuat dalam pertanyaan.

Juga, saya sarankan bahwa tidak membingungkan tujuan dengan instrumen. Seseorang ingin memastikan bahwa teknik atau metodologi yang dipilih mendukung pencapaian tujuan utama, tetapi saya tidak merasa di luar konteks yang mana teknik-adalah-diskusi terbaik sangat berguna. Memang membantu untuk mengetahui perangkap dari berbagai pendekatan bersama dengan sweet spot mereka yang jelas.

Misalnya, apa yang ingin Anda capai, apa yang Anda miliki untuk memulai, dan apa saja kendalanya?

Apakah Anda membuat kerangka kerja komponen, bahkan yang bertujuan khusus? Apakah antarmuka dapat dipisahkan dari implementasi dalam sistem pemrograman atau apakah itu dicapai dengan praktik menggunakan jenis teknologi yang berbeda? Bisakah Anda memisahkan struktur warisan antarmuka (jika ada) dari struktur warisan kelas yang mengimplementasikannya? Apakah penting untuk menyembunyikan struktur kelas suatu implementasi dari kode yang bergantung pada antarmuka yang diberikan oleh implementasi? Apakah ada beberapa implementasi yang dapat digunakan pada saat yang sama atau apakah variasi lebih dari waktu ke waktu sebagai konsekuensi dari pemeliharaan dan peningkatan perangkat? Ini dan lebih banyak perlu dipertimbangkan sebelum Anda terpaku pada alat atau metodologi.

Akhirnya, apakah penting untuk mengunci perbedaan dalam abstraksi dan bagaimana Anda memikirkannya (seperti pada is-a versus has-a) dengan fitur-fitur berbeda dari teknologi OO? Mungkin demikian, jika itu membuat struktur konseptual konsisten dan dapat dikelola untuk Anda dan orang lain. Tetapi adalah bijaksana untuk tidak diperbudak oleh hal itu dan contortions yang akhirnya Anda buat. Mungkin yang terbaik adalah mundur satu tingkat dan tidak terlalu kaku (tapi tinggalkan narasi yang baik agar orang lain tahu apa yang terjadi). [Saya mencari apa yang membuat bagian tertentu dari suatu program dapat dijelaskan, tetapi beberapa kali saya mencari keanggunan ketika ada kemenangan yang lebih besar. Tidak selalu ide terbaik.]

Saya seorang purist antarmuka, dan saya tertarik pada berbagai masalah dan pendekatan di mana purisme antarmuka sesuai, apakah membangun kerangka kerja Java atau mengatur beberapa implementasi COM. Itu tidak membuatnya cocok untuk semuanya, bahkan tidak dekat dengan segalanya, meskipun aku bersumpah demi itu. (Saya memiliki beberapa proyek yang tampaknya memberikan contoh tandingan yang serius terhadap kemurnian antarmuka, sehingga akan menarik untuk melihat bagaimana saya berhasil mengatasinya.)

orcmid
sumber
2

Saya akan membahas bagian mana-ini-mungkin-berlaku. Berikut adalah contoh keduanya, dalam skenario permainan. Misalkan, ada permainan yang memiliki berbagai jenis tentara. Setiap prajurit dapat memiliki ransel yang dapat menampung hal-hal yang berbeda.

Warisan di sini? Ada baret laut, hijau & penembak jitu. Ini adalah tipe prajurit. Jadi, ada kelas dasar Soldier dengan Marine, Green Baret & Sniper sebagai kelas turunan

Agregasi di sini? Ransel itu bisa berisi granat, senjata (berbagai jenis), pisau, medikit, dll. Seorang prajurit dapat dilengkapi dengan semua ini pada suatu titik waktu tertentu, ditambah ia juga dapat memiliki rompi anti peluru yang bertindak sebagai baju besi ketika diserang dan pasukannya. cedera berkurang hingga persentase tertentu. Kelas prajurit berisi objek kelas rompi antipeluru dan kelas ransel yang berisi referensi ke item ini.

Salman Kasbati
sumber
2

Saya pikir ini bukan perdebatan. Hanya saja:

  1. hubungan is-a (inheritance) lebih jarang terjadi daripada hubungan has-a (komposisi).
  2. Warisan lebih sulit untuk mendapatkan yang benar, bahkan ketika itu tepat untuk menggunakannya, jadi uji tuntas harus diambil karena dapat merusak enkapsulasi, mendorong penggabungan yang ketat dengan memaparkan implementasi dan sebagainya.

Keduanya memiliki tempat masing-masing, tetapi warisan lebih berisiko.

Meskipun tentu saja tidak masuk akal untuk memiliki kelas Shape 'having-a' Point dan Square. Di sini warisan sudah jatuh tempo.

Orang-orang cenderung berpikir tentang pewarisan terlebih dahulu ketika mencoba merancang sesuatu yang dapat diperluas, itulah yang salah.

Vinko Vrsalovic
sumber
1

Favor terjadi ketika kedua kandidat lolos. A dan B adalah opsi dan Anda menyukai A. Alasannya adalah komposisi menawarkan lebih banyak kemungkinan ekstensi / fleksibilitas daripada generalisasi. Ekstensi / fleksibilitas ini sebagian besar merujuk pada runtime / fleksibilitas dinamis.

Manfaatnya tidak segera terlihat. Untuk melihat manfaatnya, Anda harus menunggu permintaan perubahan tak terduga berikutnya. Jadi dalam kebanyakan kasus mereka yang bertahan pada generalisasi gagal jika dibandingkan dengan mereka yang memeluk komposisi (kecuali satu kasus yang jelas disebutkan kemudian). Karena itu aturannya. Dari sudut pandang pembelajaran jika Anda dapat mengimplementasikan injeksi ketergantungan dengan sukses, maka Anda harus tahu mana yang disukai dan kapan. Aturan membantu Anda dalam membuat keputusan juga; jika Anda tidak yakin maka pilih komposisi.

Ringkasan: Komposisi: Kopling dikurangi dengan hanya memiliki beberapa hal kecil yang Anda tancapkan ke sesuatu yang lebih besar, dan objek yang lebih besar hanya memanggil objek yang lebih kecil kembali. Generalisasi: Dari sudut pandang API, mendefinisikan bahwa suatu metode dapat diganti adalah komitmen yang lebih kuat daripada mendefinisikan bahwa suatu metode dapat dipanggil. (Sangat sedikit kesempatan ketika Generalisasi menang). Dan jangan pernah lupa bahwa dengan komposisi Anda menggunakan warisan juga, dari antarmuka, bukan kelas besar

Awan Biru
sumber
0

Kedua pendekatan digunakan untuk menyelesaikan masalah yang berbeda. Anda tidak selalu perlu mengumpulkan lebih dari dua kelas atau lebih ketika mewarisi dari satu kelas.

Kadang-kadang Anda harus mengagregasi satu kelas karena kelas itu disegel atau memiliki anggota non-virtual yang perlu Anda sadap sehingga Anda membuat lapisan proksi yang jelas tidak valid dalam hal pewarisan tetapi selama kelas yang Anda proksi memiliki antarmuka Anda dapat berlangganan ini dapat bekerja dengan baik

cfeduke
sumber