Bagaimana cara mengatasi saling ketergantungan kelas dalam kode C ++ saya?

10

Dalam proyek C ++ saya, saya punya dua kelas, Particledan Contact. Di Particlekelas, saya memiliki variabel anggota std::vector<Contact> contactsyang berisi semua kontak dari suatu Particleobjek, dan fungsi anggota yang sesuai getContacts()dan addContact(Contact cont). Jadi, dalam "Particle.h", saya memasukkan "Contact.h".

Di Contactkelas, saya ingin menambahkan kode ke konstruktor untuk Contactpanggilan itu Particle::addContact(Contact cont), sehingga contactsdiperbarui untuk kedua Particleobjek di mana Contactobjek ditambahkan. Jadi, saya harus memasukkan "Particle.h" dalam "Contact.cpp".

Pertanyaan saya adalah apakah ini dapat diterima / praktik pengkodean yang baik dan, jika tidak, apa yang akan menjadi cara yang lebih baik untuk mengimplementasikan apa yang saya coba capai (sederhananya, secara otomatis memperbarui daftar kontak untuk partikel tertentu setiap kali kontak baru dibuat).


Kelas-kelas ini akan diikat bersama oleh Networkkelas yang akan memiliki N partikel ( std::vector<Particle> particles) dan kontak Nc ( std::vector<Contact> contacts). Tapi saya ingin dapat memiliki fungsi seperti particles[0].getContacts()- apakah boleh memiliki fungsi seperti itu di Particlekelas dalam kasus ini, atau apakah ada asosiasi "struktur" yang lebih baik di C ++ untuk tujuan ini (dari dua kelas terkait yang digunakan di kelas lain) .


Saya mungkin perlu perubahan perspektif di sini tentang bagaimana saya mendekati ini. Karena dua kelas dihubungkan oleh Networkobjek kelas, apakah itu kode / kelas organisasi yang khas untuk memiliki informasi konektivitas sepenuhnya dikendalikan oleh Networkobjek (di mana objek partikel tidak harus menyadari kontaknya dan, akibatnya, seharusnya tidak memiliki getContacts()anggota fungsi). Kemudian, untuk mengetahui kontak apa yang dimiliki partikel tertentu, saya perlu mendapatkan informasi itu melalui Networkobjek (misalnya, menggunakan network.getContacts(Particle particle)).

Apakah itu kurang khas (bahkan mungkin berkecil hati) desain kelas C ++ untuk objek Partikel untuk memiliki pengetahuan itu, juga (yaitu, memiliki beberapa cara untuk mengakses informasi itu - baik melalui objek Jaringan atau objek Partikel, mana yang tampaknya lebih nyaman )?

AnInquiringMind
sumber
4
Berikut ini ceramah dari cppcon 2017 - The Three Layers of Headers: youtu.be/su9ittf-ozk
Robert Andrzantai
3
Pertanyaan yang mengandung kata-kata seperti "terbaik," "lebih baik" dan "dapat diterima" tidak dapat dijawab kecuali Anda dapat menyatakan kriteria evaluasi spesifik Anda.
Robert Harvey
Terima kasih atas hasil editnya, meskipun mengubah kata-kata Anda menjadi "tipikal" hanya membuatnya menjadi masalah popularitas. Ada alasan mengapa pengkodean dilakukan dengan satu atau lain cara, dan sementara popularitas dapat menjadi indikasi bahwa suatu teknik adalah "baik" (untuk beberapa definisi "baik"), itu juga bisa menjadi indikasi pemujaan kargo.
Robert Harvey
@RobertHarvey Saya menghapus "lebih baik" dan "buruk" di bagian terakhir saya. Saya kira saya meminta pendekatan khas (mungkin bahkan disukai / didorong) ketika Anda memiliki Networkobjek kelas yang berisi Particleobjek dan Contactobjek. Dengan pengetahuan dasar itu, saya kemudian dapat mencoba menilai apakah cocok atau tidak dengan kebutuhan spesifik saya, yang masih dieksplorasi / dikembangkan saat saya mengikuti proyek ini.
AnInquiringMind
@ RobertTarvey Saya kira saya cukup baru untuk menulis proyek C ++ sepenuhnya dari awal sehingga saya baik-baik saja dengan mempelajari apa yang "khas" dan "populer". Mudah-mudahan saya akan mendapatkan wawasan yang cukup di beberapa titik untuk dapat menyadari mengapa implementasi lain sebenarnya lebih baik, tetapi untuk saat ini, saya hanya ingin memastikan saya tidak mendekati ini dengan cara yang benar-benar sulit!
AnInquiringMind

Jawaban:

17

Ada dua bagian dalam pertanyaan Anda.

Bagian pertama adalah organisasi file header C ++ dan file sumber. Ini dipecahkan dengan menggunakan deklarasi maju dan pemisahan deklarasi kelas (meletakkannya di file header) dan metode tubuh (meletakkannya di file sumber). Lebih jauh lagi, dalam beberapa kasus seseorang dapat menerapkan idiom Pimpl ("pointer ke implementasi") untuk menyelesaikan kasus yang lebih sulit. Gunakan pointer kepemilikan bersama ( shared_ptr), pointer kepemilikan tunggal ( unique_ptr), dan pointer tidak memiliki (pointer mentah, yaitu "tanda bintang") sesuai dengan praktik terbaik.

Bagian kedua adalah bagaimana memodelkan objek yang saling terkait dalam bentuk grafik . Grafik umum yang bukan DAG (grafik asiklik langsung) tidak memiliki cara alami untuk mengekspresikan kepemilikan seperti pohon. Sebagai gantinya, semua node dan koneksi adalah metadata yang dimiliki oleh objek grafik tunggal. Dalam hal ini, tidak mungkin untuk memodelkan hubungan simpul-koneksi sebagai agregasi. Node tidak memiliki "koneksi"; koneksi tidak "memiliki" node. Sebaliknya, mereka adalah asosiasi, dan kedua node dan koneksi "dimiliki oleh" grafik. Grafik menyediakan metode kueri dan manipulasi yang beroperasi pada node dan koneksi.

rwong
sumber
Terima kasih atas tanggapannya! Saya sebenarnya memiliki kelas Jaringan yang akan memiliki N partikel dan kontak Nc. Tapi saya ingin dapat memiliki fungsi seperti particles[0].getContacts()- apakah Anda menyarankan dalam paragraf terakhir Anda bahwa saya seharusnya tidak memiliki fungsi seperti itu di Particlekelas, atau bahwa struktur saat ini baik-baik saja karena mereka terkait / terkait secara inheren melalui Network? Apakah ada "struktur" asosiasi yang lebih baik dalam C ++ dalam kasus ini?
AnInquiringMind
1
Secara umum bahwa Jaringan bertanggung jawab untuk mengetahui hubungan antar objek. Jika Anda menggunakan daftar adjacency, misalnya, partikel network.particle[p]akan memiliki yang sesuai network.contacts[p]dengan indeks kontaknya. Kalau tidak, Jaringan dan Partikel entah bagaimana melacak informasi yang sama.
berguna
@ Tidak Berguna Ya, di situlah saya tidak yakin bagaimana melanjutkan. Jadi, Anda mengatakan bahwa Particleobjek tidak harus mengetahui kontaknya (jadi saya tidak boleh memiliki getContacts()fungsi anggota), dan bahwa informasi itu seharusnya hanya berasal dari dalam Networkobjek? Apakah ini akan menjadi desain kelas C ++ yang buruk untuk Particleobjek yang memiliki pengetahuan itu (yaitu, memiliki beberapa cara untuk mengakses informasi itu - baik melalui Networkobjek atau Particleobjek, mana yang lebih nyaman)? Yang terakhir tampaknya lebih masuk akal bagi saya, tetapi mungkin saya perlu mengubah perspektif saya tentang ini.
AnInquiringMind
1
@PhysicsCodingEnthusiast: Masalah dengan Particlemengetahui apa-apa tentang Contacts atau Networks adalah ia mengikat Anda dengan cara tertentu untuk mewakili hubungan itu. Ketiga kelas mungkin harus setuju. Jika bukan satu Network-satunya yang tahu atau peduli, itu hanya satu kelas yang perlu diubah jika Anda memutuskan representasi lain lebih baik.
cao
@ cHao Oke, ini masuk akal. Jadi Particledan Contactharus benar-benar terpisah, dan hubungan di antara mereka ditentukan oleh Networkobjek. Hanya untuk benar-benar yakin, inilah (mungkin) yang dimaksud dengan @rwong ketika ia menulis, "baik node maupun koneksi" dimiliki oleh "grafik. Grafik menyediakan metode kueri dan manipulasi yang beroperasi pada node dan koneksi." , Baik?
AnInquiringMind
5

Jika saya membuat Anda benar, objek kontak yang sama milik lebih dari satu objek partikel, karena itu mewakili semacam kontak fisik antara dua atau lebih partikel, kan?

Jadi hal pertama yang menurut saya patut dipertanyakan adalah mengapa Particlememiliki variabel anggota std::vector<Contact>? Seharusnya a std::vector<Contact*>atau std::vector<std::shared_ptr<Contact> >sebaliknya. addContactmaka harus memiliki tanda tangan berbeda seperti addContact(Contact *cont)atau addContact(std::shared_ptr<Contact> cont)sebagai gantinya.

Ini membuatnya tidak perlu untuk memasukkan "Contact.h" dalam "Particle.h", sebuah deklarasi maju class Contactdalam "Particle.h", dan menyertakan "Contact.h" dalam "Particle.cpp" sudah cukup.

Lalu pertanyaan tentang konstruktor. Anda menginginkan sesuatu seperti

 Contact(Particle &p1, Particle &p2)
 {
      p1.addContact(this);
      p2.addContact(this);
 }

Baik? Desain ini ok, asalkan program Anda selalu mengetahui partikel terkait pada titik waktu ketika objek kontak harus dibuat.

Catatan, jika Anda pergi std::vector<Contact*>rute, Anda harus menginvestasikan beberapa pemikiran tentang masa hidup dan kepemilikan Contactobjek. Tidak ada partikel "memiliki" kontaknya, kontak mungkin harus dihapus hanya jika kedua Particleobjek terkait dihancurkan. std::shared_ptr<Contact>Sebaliknya, menggunakan akan menyelesaikan masalah ini untuk Anda secara otomatis. Atau Anda membiarkan objek "konteks sekitarnya" mengambil kepemilikan partikel dan kontak (seperti yang disarankan oleh @rwong), dan mengelola masa pakainya.

Doc Brown
sumber
Saya tidak melihat manfaat dari addContact(const std::shared_ptr<Contact> &cont)atas addContact(std::shared_ptr<Contact> cont)?
Caleth
@ Caleth: ini telah dibahas di sini: stackoverflow.com/questions/3310737/... - "const" tidak terlalu penting di sini, tetapi melewati objek dengan referensi (dan skalar berdasarkan nilai) adalah idiom standar dalam C ++.
Doc Brown
2
Banyak dari jawaban itu tampaknya berasal dari pra- moveparadigma
Caleth
@ Caleth: ok, untuk membuat semua nitpicker senang, saya mengubah bagian yang tidak penting dari jawaban saya.
Doc Brown
1
@PhysicsCodingEnthusiast: tidak, ini yang terpenting tentang membuat particle1.getContacts()dan particle2.getContacts()mengirimkan Contactobjek yang sama yang mewakili kontak fisik antara particle1dan particle2, dan bukan dua objek yang berbeda. Tentu saja, seseorang dapat mencoba mendesain sistem dengan cara yang tidak masalah jika ada dua Contactobjek yang tersedia sekaligus mewakili kontak fisik yang sama. Ini akan melibatkan untuk membuat Contactkekekalan, tetapi apakah Anda yakin ini yang Anda inginkan?
Doc Brown
0

Ya, apa yang Anda gambarkan adalah cara yang dapat diterima untuk memastikan bahwa setiap Contactinstance ada dalam daftar kontak a Particle.

Bart van Ingen Schenau
sumber
Terima kasih atas tanggapannya. Saya telah membaca beberapa saran bahwa memiliki sepasang kelas yang saling bergantung harus dihindari (misalnya, dalam "Pola Desain C + + dan Harga Derivatif" oleh MS Joshi), tetapi ternyata itu belum tentu benar? Karena penasaran, apakah mungkin ada cara lain untuk menerapkan pembaruan otomatis ini tanpa memerlukan saling ketergantungan?
AnInquiringMind
4
@PhysicsCodingEnthusiast: Memiliki kelas yang saling tergantung menciptakan semua jenis kesulitan dan Anda harus mencoba menghindarinya. Tetapi kadang-kadang, dua kelas sangat erat terkait satu sama lain sehingga menghilangkan saling ketergantungan di antara mereka menyebabkan lebih banyak masalah daripada saling ketergantungan itu sendiri.
Bart van Ingen Schenau
0

Apa yang Anda lakukan benar.

Cara lain ... Jika tujuannya adalah untuk memastikan bahwa setiap orang Contactada dalam daftar, maka Anda dapat:

  • blokir penciptaan Contact (konstruktor pribadi),
  • maju menyatakan Particle kelas,
  • Jadikan Particlekelas temanContact ,
  • dalam Particlemembuat metode pabrik yang membuat aContact

Maka Anda tidak harus menyertakan particle.hdicontact

Robert Andrzantai
sumber
Terima kasih atas tanggapannya! Itu sepertinya cara yang bermanfaat untuk mengimplementasikan ini. Hanya ingin tahu, dengan edit saya pada pertanyaan awal mengenai Networkkelas, apakah itu mengubah struktur yang disarankan, atau apakah masih sama?
AnInquiringMind
Setelah Anda memperbarui pertanyaan Anda, itu mengubah ruang lingkup. ... Sekarang Anda bertanya tentang arsitektur aplikasi Anda, padahal sebelumnya itu tentang masalah teknis.
Robert Andrzantai
0

Opsi lain yang mungkin Anda pertimbangkan adalah membuat konstruktor Kontak yang menerima referensi Partikel templated. Ini akan memungkinkan Kontak untuk menambahkan dirinya sendiri ke wadah apa pun yang mengimplementasikan addContact(Contact).

template<class Container>
Contact(/*parameters*/, Container& container)
{
  container.addContact(*this);
}
Keliru
sumber