Apa cara yang baik untuk mewakili hubungan banyak-ke-banyak antara dua kelas?

10

Katakanlah saya memiliki dua jenis objek, A dan B. Hubungan di antara mereka banyak-ke-banyak, tetapi tidak satu pun dari mereka adalah pemilik yang lain.

Kedua instance A dan B harus mewaspadai koneksi; itu bukan hanya satu arah.

Jadi, kita bisa melakukan ini:

class A
{
    ...

    private: std::vector<B *> Bs;
}

class B
{
    private: std::vector<A *> As;
}

Pertanyaan saya adalah: di mana saya meletakkan fungsi untuk membuat dan menghancurkan koneksi?

Haruskah itu A :: Attach (B), yang kemudian memperbarui A :: Bs dan B :: As vektor?

Atau harus B :: Lampirkan (A), yang tampaknya sama-sama masuk akal.

Tak satu pun dari mereka terasa benar. Jika saya berhenti bekerja dengan kode, dan kembali setelah seminggu, saya yakin saya tidak akan dapat mengingat jika saya harus melakukan A.Attach (B) atau B.Attach (A).

Mungkin itu harus fungsi seperti ini:

CreateConnection(A, B);

Tetapi membuat fungsi global juga tampaknya tidak diinginkan, mengingat itu adalah fungsi khusus untuk bekerja dengan hanya kelas A dan B.

Pertanyaan lain: jika saya sering mengalami masalah / persyaratan ini, bisakah saya membuat solusi umum untuk itu? Mungkin kelas TwoWayConnection yang bisa saya peroleh atau gunakan dalam kelas yang berbagi jenis hubungan ini?

Apa sajakah cara yang baik untuk menangani situasi ini ... Saya tahu cara menangani situasi satu-ke-banyak "C memiliki D" dengan cukup baik, tetapi yang satu ini lebih rumit.

Sunting: Hanya untuk membuatnya lebih eksplisit, pertanyaan ini tidak melibatkan masalah kepemilikan. Baik A dan B dimiliki oleh beberapa objek Z lainnya, dan Z menangani semua masalah kepemilikan. Saya hanya tertarik dengan cara membuat / menghapus tautan banyak-ke-banyak antara A dan B.

Dmitri Shuralyov
sumber
Saya akan membuat Z :: Lampirkan (A, B), jadi, jika Z memiliki dua jenis objek, "terasa benar", baginya untuk membuat koneksi. Dalam contoh saya (tidak begitu baik), Z akan menjadi repositori untuk A dan B. Saya tidak bisa melihat cara lain tanpa contoh praktis.
Machado
1
Lebih tepatnya, A dan B mungkin memiliki pemilik yang berbeda. Mungkin A dimiliki oleh Z, sedangkan B dimiliki oleh X, yang pada gilirannya dimiliki oleh Z. Seperti yang Anda lihat, kemudian menjadi benar-benar berbelit-belit mencoba mengelola tautan di tempat lain. A dan B harus dapat dengan cepat mengakses daftar pasangan yang terhubung dengannya.
Dmitri Shuralyov
Jika Anda ingin tahu tentang nama asli A dan B dalam situasi saya, mereka adalah Pointerdan GestureRecognizer. Pointer dimiliki dan dikelola oleh kelas InputManager. GestureRecognizers dimiliki oleh instance Widget, yang pada gilirannya dimiliki oleh instance layar yang dimiliki oleh instance App. Pointer ditugaskan ke GestureRecognizers sehingga mereka dapat mengumpankan data input mentah kepada mereka, tetapi GestureRecognizers perlu mengetahui berapa banyak pointer yang ada saat ini yang dikaitkan dengan mereka (untuk membedakan 1 jari vs 2 gerakan jari, dll.).
Dmitri Shuralyov
Sekarang saya lebih memikirkannya, sepertinya saya hanya mencoba melakukan pengenalan gerakan berbasis bingkai (bukan berbasis pointer). Jadi saya mungkin juga hanya melewati acara untuk memberi isyarat frame-at-a-time daripada pointer-at-a-time. Hmm.
Dmitri Shuralyov

Jawaban:

2

Salah satu caranya adalah dengan menambahkan Attach()metode publik dan juga AttachWithoutReciprocating()metode yang dilindungi ke setiap kelas. Buat Adan Bsaling berteman sehingga Attach()metode mereka dapat memanggil yang lain AttachWithoutReciprocating():

A::Attach(B &b) {
    Bs.push_back(&b);
    b.AttachWithoutReciprocating(*this);
}

A::AttachWithoutReciprocating(B &b) {
    Bs.push_back(&b);
}

Jika Anda menerapkan metode serupa untuk B, Anda tidak perlu mengingat kelas mana yang harus dipanggil Attach().

Saya yakin Anda dapat membungkus perilaku itu dalam MutuallyAttachablekelas yang mewarisi Adan sekaligus Bmewarisi, sehingga menghindari pengulangan dan mencetak poin bonus pada Hari Penghakiman. Tetapi bahkan pendekatan implement-it-di-kedua-tempat yang tidak canggih akan menyelesaikan pekerjaan.

Caleb
sumber
1
Ini kedengarannya persis seperti solusi yang saya pikirkan di benak saya (yaitu menempatkan Attach () di A dan B). Saya juga sangat menyukai solusi yang KERING (saya benar-benar tidak suka kode duplikasi) memiliki kelas MutuallyAttachable yang dapat Anda peroleh dari setiap kali perilaku ini diperlukan (saya memperkirakan cukup sering). Hanya dengan melihat solusi yang sama yang ditawarkan oleh orang lain membuat saya jauh lebih percaya diri di dalamnya, terima kasih!
Dmitri Shuralyov
Menerima ini karena IMO itu solusi terbaik yang ditawarkan sejauh ini. Terima kasih! Saya akan mengimplementasikan kelas MutuallyAttachable sekarang dan melaporkan di sini jika saya mengalami masalah yang tidak terduga (beberapa kesulitan mungkin juga timbul karena pewarisan berganda sekarang yang saya belum punya banyak pengalaman).
Dmitri Shuralyov
Semuanya berjalan lancar. Namun, sesuai komentar terakhir saya pada pertanyaan awal, saya mulai menyadari bahwa saya tidak memerlukan fungsionalitas ini untuk apa yang awalnya saya bayangkan. Alih-alih melacak koneksi 2 arah ini, saya hanya akan melewatkan setiap pasangan sebagai parameter ke fungsi yang membutuhkannya. Namun, ini mungkin masih berguna di lain waktu.
Dmitri Shuralyov
Inilah MutuallyAttachablekelas yang saya tulis untuk tugas ini, jika ada yang ingin menggunakannya kembali: goo.gl/VY9RB (Pastebin) Edit: Kode ini dapat ditingkatkan dengan menjadikannya kelas templat.
Dmitri Shuralyov
1
Saya telah menempatkan MutuallyAttachable<T, U>templat kelas di Gist. gist.github.com/3308058
Dmitri Shuralyov
5

Apakah mungkin bagi hubungan itu sendiri untuk memiliki properti tambahan?

Jika demikian, maka itu harus menjadi kelas yang terpisah.

Jika tidak, maka mekanisme manajemen daftar yang saling berbalas akan cukup.

Steven A. Lowe
sumber
Jika ini adalah kelas yang terpisah, bagaimana A bisa tahu instance terkait dari B, dan B tahu instance terkait dari A? Pada titik itu, baik A dan B perlu memiliki hubungan 1-ke-banyak dengan C, bukan?
Dmitri Shuralyov
1
A dan B keduanya membutuhkan referensi ke koleksi instance C; C melayani tujuan yang sama sebagai 'tabel jembatan' dalam pemodelan relasional
Steven A. Lowe
1

Biasanya ketika saya masuk ke situasi seperti ini yang terasa canggung, tetapi saya tidak tahu mengapa, itu karena saya mencoba untuk menempatkan sesuatu di kelas yang salah. Hampir selalu ada cara untuk memindahkan beberapa fungsionalitas keluar dari kelas Adan Buntuk membuat semuanya lebih jelas.

Salah satu kandidat untuk memindahkan asosiasi adalah kode yang menciptakan asosiasi di tempat pertama. Mungkin alih-alih menyebutnya attach()hanya menyimpan daftar std::pair<A, B>. Jika ada beberapa tempat yang membuat asosiasi itu dapat disarikan menjadi satu kelas.

Kandidat lain untuk bergerak adalah objek yang memiliki objek terkait, Zdalam kasus Anda.

Kandidat lain yang umum untuk memindahkan asosiasi adalah kode yang sering memanggil metode Aatau Bobjek. Misalnya, alih-alih menelepon a->foo(), yang secara internal melakukan sesuatu dengan semua Bobjek terkait , ia sudah tahu asosiasi dan panggilan a->foo(b)untuk masing-masing. Sekali lagi, jika beberapa tempat menyebutnya, ia dapat diabstraksi menjadi satu kelas.

Banyak kali objek yang membuat, memiliki, dan menggunakan asosiasi terjadi pada objek yang sama. Itu bisa membuat refactoring sangat mudah.

Metode terakhir adalah untuk mendapatkan hubungan dari hubungan lain. Misalnya, saudara dan saudari adalah hubungan banyak-ke-banyak, tetapi alih-alih menyimpan daftar saudari di kelas saudara dan sebaliknya, Anda memperoleh hubungan itu dari hubungan orang tua. Keuntungan lain dari metode ini adalah ia menciptakan satu sumber kebenaran. Tidak ada cara yang mungkin untuk kesalahan bug atau runtime untuk membuat perbedaan antara daftar saudara, saudari, dan orangtua, karena Anda hanya memiliki satu daftar.

Refactoring semacam ini bukanlah formula ajaib yang berfungsi setiap saat. Anda masih harus bereksperimen sampai Anda menemukan kecocokan untuk setiap keadaan individu, tetapi saya telah menemukan bahwa itu berfungsi sekitar 95% dari waktu. Sisa waktu Anda hanya harus hidup dengan canggung.

Karl Bielefeldt
sumber
0

Jika saya menerapkan ini, saya akan meletakkan Lampirkan di kedua A dan B. Di dalam metode Lampirkan, saya kemudian akan memanggil Lampirkan pada objek yang lewat. Dengan begitu Anda dapat memanggil A.Attach (B) atau B.Attach ( SEBUAH). Sintaks saya mungkin tidak benar, saya belum pernah menggunakan c ++ selama bertahun-tahun:

class A {
  private: std::vector<B *> Bs;

  void Attach (B* obj) {
    // Only add obj if it's not in the vector
    if (std::find(Bs.begin(), Bs.end(), obj) == Bs.end()) {
      //Add obj to the vector
      Bs.push_back(obj);

      // Attach this class to obj
      obj->Attach(this);
    }
  }    
}

class B {
  private: std::vector<A *> As;

  void Attach (A* obj) {
    // Only add obj if it's not in the vector
    if (std::find(As.begin(), As.end(), obj) == As.end()) {
      //Add obj to the vector
      As.push_back(obj);

      // Attach this class to obj
      obj->Attach(this);
    }
  }    
}

Metode alternatif adalah membuat kelas 3, C yang mengelola daftar statis pasangan AB.

pengantin
sumber
Hanya pertanyaan mengenai saran alternatif Anda untuk memiliki kelas 3 C untuk mengelola daftar pasangan AB: Bagaimana A dan B mengetahui anggota pasangannya? Apakah masing-masing A dan B memerlukan tautan ke (tunggal) C, lalu meminta C untuk menemukan pasangan yang sesuai? B :: Foo () {std :: vector <A *> As = C.GetAs (ini); / * Lakukan sesuatu dengan As ... * /} Atau apakah Anda membayangkan sesuatu yang lain? Karena ini sepertinya berbelit-belit.
Dmitri Shuralyov