Jaki jest dobry sposób na przedstawienie relacji wiele do wielu między dwiema klasami?

10

Powiedzmy, że mam dwa typy obiektów, A i B. Relacje między nimi są wiele do wielu, ale żaden z nich nie jest właścicielem drugiego.

Zarówno wystąpienia A, jak i B muszą być świadome połączenia; to nie tylko jeden sposób.

Możemy to zrobić:

class A
{
    ...

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

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

Moje pytanie brzmi: gdzie mam umieścić funkcje do tworzenia i niszczenia połączeń?

Czy powinien to być A :: Attach (B), który następnie aktualizuje A :: Bs i B :: As wektory?

A może powinien to być B :: Attach (A), co wydaje się równie rozsądne.

Żadne z tych nie wydaje się właściwe. Jeśli przestanę pracować z kodem i wrócę po tygodniu, jestem pewien, że nie będę w stanie przypomnieć sobie, czy powinienem wykonywać A.Attach (B) lub B.Attach (A).

Być może powinna to być taka funkcja:

CreateConnection(A, B);

Ale tworzenie funkcji globalnej również wydaje się niepożądane, biorąc pod uwagę, że jest to funkcja specjalnie do pracy tylko z klasami A i B.

Kolejne pytanie: jeśli często napotykam ten problem / wymaganie, czy mogę w jakiś sposób znaleźć ogólne rozwiązanie tego problemu? Być może klasa TwoWayConnection, z której mogę czerpać lub korzystać z klas dzielących ten rodzaj relacji?

Jakie są dobre sposoby poradzenia sobie z tą sytuacją ... Wiem, jak całkiem dobrze poradzić sobie z sytuacją „C jest właścicielem D” jeden do wielu, ale ta jest trudniejsza.

Edycja: aby wyjaśnić, to pytanie nie wiąże się z problemami własnościowymi. Zarówno A, jak i B są własnością jakiegoś innego obiektu Z, a Z zajmuje się wszystkimi kwestiami własności. Interesuje mnie tylko sposób tworzenia / usuwania połączeń wiele do wielu między A i B.

Dmitrij Shuralyov
źródło
Stworzyłbym Z :: Attach (A, B), więc jeśli Z jest właścicielem dwóch rodzajów obiektów, „czuje się dobrze”, aby mógł utworzyć połączenie. W moim (niezbyt dobrym) przykładzie Z byłoby repozytorium dla A i B. Nie widzę innego sposobu bez praktycznego przykładu.
Machado
1
Mówiąc ściślej, A i B mogą mieć różnych właścicieli. Być może A jest własnością Z, podczas gdy B jest własnością X, który z kolei jest własnością Z. Jak widać, wtedy staje się bardzo skomplikowany, próbując zarządzać łączem gdzie indziej. A i B muszą mieć szybki dostęp do listy par, z którymi są połączone.
Dmitri Shuralyov
Jeśli jesteś ciekawy prawdziwych nazw A i B w mojej sytuacji, to są Pointeri GestureRecognizer. Wskaźniki są własnością klasy InputManager i są przez nią zarządzane. GestureRecognizers są własnością instancji Widget, które z kolei są własnością instancji Screen, która jest własnością instancji aplikacji. Wskaźniki są przypisywane do GestureRecognizers, aby mogli przekazywać im surowe dane wejściowe, ale GestureRecognizers muszą być świadomi, ile wskaźników jest obecnie z nimi powiązanych (aby odróżnić gesty 1 palcem od 2 palców itp.).
Dmitri Shuralyov,
Teraz, gdy myślę o tym więcej, wydaje mi się, że próbuję po prostu rozpoznać gesty oparte na ramce (a nie na wskaźniku). Równie dobrze mógłbym przekazywać zdarzenia gestami klatka po klatce zamiast wskazywania palcem. Hmm
Dmitri Shuralyov,

Odpowiedzi:

2

Jednym ze sposobów jest dodanie do każdej klasy Attach()metody publicznej i chronionej AttachWithoutReciprocating(). Zrobić Ai Bwspólnych znajomych tak, że ich Attach()metody mogą dzwonić Innego AttachWithoutReciprocating():

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

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

Jeśli zastosujesz podobne metody B, nie będziesz musiał pamiętać, do której klasy zadzwonić Attach().

Jestem pewien, że możesz podsumować to zachowanie w MutuallyAttachableklasie, która zarówno dziedziczy , jak Ai Bdziedziczy, unikając w ten sposób powtarzania się i zdobywania punktów bonusowych w Dzień Sądu. Ale nawet niewyszukane podejście typu „wdrażaj w oba miejsca” wykona zadanie.

Caleb
źródło
1
To brzmi dokładnie jak rozwiązanie, o którym myślałem w głębi duszy (tj. Umieścić Attach () zarówno w A, jak i B). Bardzo podoba mi się również bardziej SUCHE rozwiązanie (naprawdę nie lubię duplikowania kodu) posiadania klasy Wzajemnie Dołączalnej, z której można czerpać, ilekroć takie zachowanie jest potrzebne (przewiduję dość często). Sam widok tego samego rozwiązania oferowanego przez kogoś innego sprawia, że ​​jestem o wiele bardziej pewny, dzięki!
Dmitri Shuralyov,
Akceptując to, ponieważ IMO jest najlepszym dotychczas oferowanym rozwiązaniem. Dzięki! Zamierzam teraz wdrożyć tę klasę Wzajemnie Dołączalną i zgłoś tutaj, jeśli napotkam jakieś nieprzewidziane problemy (mogą pojawić się pewne trudności z powodu wielokrotnego dziedziczenia, z którym nie mam jeszcze dużego doświadczenia).
Dmitri Shuralyov,
Wszystko poszło gładko. Jednak, zgodnie z moim ostatnim komentarzem do pierwotnego pytania, zaczynam zdawać sobie sprawę, że nie potrzebuję tej funkcjonalności do tego, co pierwotnie przewidywałem. Zamiast śledzić te dwukierunkowe połączenia, przekażę każdą parę jako parametr funkcji, która jej potrzebuje. Może się to jednak przydać później.
Dmitri Shuralyov,
Oto MutuallyAttachableklasa, którą napisałem do tego zadania, jeśli ktoś chce go ponownie użyć: goo.gl/VY9RB (Pastebin) Edycja: Ten kod można ulepszyć, czyniąc go klasą szablonów.
Dmitri Shuralyov,
1
Umieściłem MutuallyAttachable<T, U>szablon zajęć w Gist. gist.github.com/3308058
Dmitri Shuralyov,
5

Czy sama relacja może mieć dodatkowe właściwości?

Jeśli tak, to powinna to być osobna klasa.

Jeśli nie, wystarczy dowolny mechanizm zarządzania listami posuwisto-zwrotnymi.

Steven A. Lowe
źródło
Jeśli jest to osobna klasa, to skąd A miałby wiedzieć o swoich połączonych instancjach B, a B miałby wiedzieć o swoich powiązanych instancjach A? W tym momencie zarówno A, jak i B musiałyby mieć relację jeden do wielu z C, prawda?
Dmitri Shuralyov,
1
A i B potrzebowałyby odniesienia do zbioru instancji C; C służy temu samemu celowi, co „tabela pomostowa” w modelowaniu relacyjnym
Steven A. Lowe
1

Zwykle, gdy wpadam w takie sytuacje, które wydają mi się dziwne, ale nie jestem w stanie powiedzieć dlaczego, to dlatego, że próbuję umieścić coś w niewłaściwej klasie. Prawie zawsze istnieje sposób, aby przenieść niektóre funkcje poza klasę Ai Bwyjaśnić.

Jednym z kandydatów do przeniesienia powiązania jest kod, który tworzy powiązanie w pierwszej kolejności. Może zamiast zadzwonić attach(), po prostu przechowuje listę std::pair<A, B>. Jeśli istnieje wiele miejsc tworzących skojarzenia, można je wydzielić na jedną klasę.

Innym kandydatem do przeniesienia jest Zw twoim przypadku obiekt, który jest właścicielem powiązanych obiektów .

Innym częstym kandydatem do przeniesienia skojarzenia jest kod, który często wywołuje metody Alub Bobiekty. Na przykład zamiast wywoływać a->foo(), które wewnętrznie robi coś ze wszystkimi powiązanymi Bobiektami, zna już skojarzenia i wezwania a->foo(b)dla każdego z nich. Ponownie, jeśli nazywa to wiele miejsc, można je wyodrębnić w jedną klasę.

Wiele razy obiekty, które tworzą, posiadają i wykorzystują powiązanie, są tym samym obiektem. To może bardzo ułatwić refaktoryzację.

Ostatnią metodą jest wyprowadzenie relacji z innych relacji. Na przykład bracia i siostry są związkami wielu do wielu, ale zamiast utrzymywać listę sióstr w klasie braci i odwrotnie, wyprowadzasz ten związek ze związku rodzicielskiego. Inną zaletą tej metody jest to, że tworzy ona jedno źródło prawdy. Nie ma możliwości, aby błąd lub błąd w czasie wykonywania utworzył rozbieżność między listami brata, siostry i rodzica, ponieważ masz tylko jedną listę.

Ten rodzaj refaktoryzacji nie jest magiczną formułą, która działa za każdym razem. Nadal musisz eksperymentować, aż znajdziesz odpowiednie dopasowanie do każdej indywidualnej okoliczności, ale okazało się, że działa to w około 95% przypadków. Pozostały czas musisz po prostu żyć z niezręcznością.

Karl Bielefeldt
źródło
0

Gdybym to zaimplementował, umieściłbym Attach w A i B. Wewnątrz metody Attach wywołałbym Attach na przekazanym obiekcie. W ten sposób można wywołać A.Attach (B) lub B.Attach ( ZA). Moja składnia może być niepoprawna, od lat nie używałem c ++:

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);
    }
  }    
}

Alternatywną metodą byłoby utworzenie trzeciej klasy, C, która zarządza statyczną listą par AB.

briddums
źródło
Tylko pytanie dotyczące Twojej alternatywnej sugestii posiadania trzeciej klasy C do zarządzania listą par AB: Skąd A i B znają członków tej pary? Czy każdy A i B potrzebuje linku do (pojedynczego) C, a następnie poprosi C o znalezienie odpowiednich par? B :: Foo () {std :: vector <A *> As = C.GetAs (this); / * Zrób coś z As ... * /} A może wyobrażałeś sobie coś innego? Ponieważ wydaje się to strasznie skomplikowane.
Dmitri Shuralyov,