Jak rozwiązać współzależność klas w moim kodzie C ++?

10

W moim projekcie C ++ mam dwie klasy Particlei Contact. W Particleklasie mam zmienną składową, std::vector<Contact> contactsktóra zawiera wszystkie kontakty Particleobiektu oraz odpowiednie funkcje składowe getContacts()i addContact(Contact cont). Zatem w „Particle.h” dołączam „Contact.h”.

W Contactklasie chciałbym dodać kod do konstruktora, aby Contactten wywołał Particle::addContact(Contact cont), aby contactsbył aktualizowany zarówno dla Particleobiektów, między którymi Contactobiekt jest dodawany. Dlatego musiałbym dołączyć „Particle.h” do „Contact.cpp”.

Moje pytanie brzmi, czy jest to akceptowalna / dobra praktyka kodowania, a jeśli nie, jaki byłby lepszy sposób na wdrożenie tego, co próbuję osiągnąć (po prostu, automatycznie aktualizując listę kontaktów dla określonej cząstki za każdym razem, gdy nowy kontakt jest tworzone).


Klasy te zostaną połączone razem Networkklasą, która będzie miała cząsteczki N ( std::vector<Particle> particles) i styki Nc ( std::vector<Contact> contacts). Ale chciałem particles[0].getContacts()mieć takie funkcje - czy Particlew tym przypadku można mieć takie funkcje w klasie, czy też istnieje lepsza „struktura” asocjacji w C ++ do tego celu (z dwóch powiązanych klas używanych w innej klasie) .


Może potrzebuję zmiany perspektywy w tym, w jaki sposób do tego podchodzę. Ponieważ obie klasy są połączone przez Networkobiekt klasy, czy typową organizacją kodu / klasy jest posiadanie przez Networkobiekt pełnej kontroli informacji o łączności (w tym sensie, że obiekt cząstek nie powinien być świadomy swoich kontaktów, a zatem nie powinien mieć getContacts()członka funkcjonować). Następnie, aby wiedzieć, jakie kontakty ma konkretna cząstka, musiałbym uzyskać tę informację przez Networkobiekt (np. Za pomocą network.getContacts(Particle particle)).

Czy mniej typowy (być może nawet zniechęcony) projekt klasy C ++ dla obiektu Particle miałby również tę wiedzę (tj. Miałby wiele sposobów dostępu do tych informacji - za pośrednictwem obiektu Network lub obiektu Particle, w zależności od tego, co wydaje się wygodniejsze )?

AnInquiringMind
źródło
4
Oto wykład z cppcon 2017 - Trzy warstwy nagłówków: youtu.be/su9ittf-ozk
Robert Andrzejuk
3
Pytania zawierające słowa „najlepszy”, „lepszy” i „akceptowalny” są bez odpowiedzi, chyba że możesz podać swoje szczegółowe kryteria oceny.
Robert Harvey
Dzięki za edycję, choć zmiana sformułowania na „typowy” sprawia, że ​​jest to kwestia popularności. Istnieją powody, dla których kodowanie odbywa się w taki czy inny sposób, i chociaż popularność może wskazywać, że technika jest „dobra” (dla niektórych definicji „dobra”), może być również oznaką kultywacji ładunków.
Robert Harvey,
@RobertHarvey Usunąłem „lepsze” i „złe” w mojej ostatniej sekcji. Przypuszczam, że proszę o typowe (być może nawet uprzywilejowane / zachęcane) podejście, gdy masz Networkobiekt klasy, który zawiera Particleobiekty i Contactobiekty. Mając tę ​​podstawową wiedzę, mogę następnie spróbować ocenić, czy pasuje ona do moich konkretnych potrzeb, które wciąż są badane / rozwijane w miarę rozwoju projektu.
AnInquiringMind
@RobertHarvey Przypuszczam, że jestem na tyle nowy, by pisać projekty C ++ całkowicie od zera, że ​​dobrze rozumiem, co jest „typowe” i „popularne”. Mam nadzieję, że w pewnym momencie zdobędę wystarczającą wiedzę, aby zdać sobie sprawę, dlaczego kolejna implementacja jest rzeczywiście lepsza, ale na razie chcę się upewnić, że nie podchodzę do tego w całkowicie kościsty sposób!
AnInquiringMind

Odpowiedzi:

17

Twoje pytanie składa się z dwóch części.

Pierwsza część to organizacja plików nagłówkowych C ++ i plików źródłowych. Aby rozwiązać ten problem, należy użyć deklaracji forward i oddzielić deklarację klasy (umieszczając je w pliku nagłówkowym) i treść metody (umieszczając je w pliku źródłowym). Ponadto w niektórych przypadkach można zastosować idiom Pimpl („wskaźnik do implementacji”), aby rozwiązać trudniejsze przypadki. Używaj wskaźników współwłasności ( shared_ptr), wskaźników pojedynczego prawa własności ( unique_ptr) i wskaźników niebędących właścicielami (wskaźnik nieprzetworzony, tj. „Gwiazdka”) zgodnie z najlepszymi praktykami.

Druga część dotyczy modelowania obiektów powiązanych ze sobą w formie wykresu . Ogólne wykresy, które nie są DAG (ukierunkowane wykresy acykliczne), nie mają naturalnego sposobu wyrażania własności drzewiastych. Zamiast tego wszystkie węzły i połączenia są metadanymi należącymi do jednego obiektu wykresu. W takim przypadku nie można modelować relacji połączenie-węzeł jako agregacji. Węzły nie „posiadają” połączeń; połączenia nie „posiadają” węzłów. Zamiast tego są to skojarzenia, a zarówno węzły, jak i połączenia są „własnością” wykresu. Wykres przedstawia metody zapytań i manipulacji, które działają na węzłach i połączeniach.

rwong
źródło
Dzięki za odpowiedzi! Tak naprawdę mam klasę sieci, która będzie miała cząsteczki N i kontakty Nc. Ale chciałem mieć funkcje takie jak particles[0].getContacts()- czy sugerujesz w swoim ostatnim akapicie, że nie powinienem mieć takich funkcji w Particleklasie, czy też obecna struktura jest w porządku, ponieważ są one ze sobą powiązane / powiązane Network? Czy w tym przypadku jest lepsza „struktura” asocjacji w C ++?
AnInquiringMind,
1
Ogólnie rzecz biorąc, sieć jest odpowiedzialna za wiedzę o relacjach między obiektami. Na przykład, jeśli użyjesz listy sąsiedztwa, cząstka network.particle[p]będzie odpowiadać network.contacts[p]wskaźnikom jej kontaktów. W przeciwnym razie sieć i cząsteczki w jakiś sposób śledzą te same informacje.
Bezużyteczne
@ Bez sensu Tak, nie jestem pewien, jak postępować. Mówisz więc, że Particleobiekt nie powinien być świadomy swoich kontaktów (więc nie powinienem mieć getContacts()funkcji członka) i że ta informacja powinna pochodzić tylko z Networkobiektu? Czy źle byłoby zaprojektować klasę C ++ dla Particleobiektu, który miałby tę wiedzę (tj. Miałby wiele sposobów dostępu do tej informacji - za pośrednictwem Networkobiektu lub Particleobiektu, w zależności od tego, co wydaje się wygodniejsze)? To drugie wydaje mi się bardziej sensowne, ale może muszę zmienić moje zdanie na ten temat.
AnInquiringMind
1
@PhysicsCodingEnthusiast: Problem ze Particleznajomością czegokolwiek na temat Contacts lub Networks polega na tym, że wiąże cię to z określonym sposobem reprezentowania tego związku. Wszystkie trzy klasy mogą musieć się zgodzić. Jeśli zamiast tego Networkjest jedynym, który wie lub dba o to, to tylko jedna klasa musi się zmienić, jeśli uznasz, że inna reprezentacja jest lepsza.
cHao
@ cHao Dobra, to ma sens. Więc Particlei Contactpowinien być całkowicie oddzielny, a powiązanie między nimi jest zdefiniowane przez Networkobiekt. Aby być całkowicie pewnym, to (prawdopodobnie) miał na myśli @rwong, kiedy napisał: „zarówno węzły, jak i połączenia są„ własnością ”wykresu. Wykres zapewnia metody zapytań i manipulacji, które działają na węzłach i połączeniach”. , dobrze?
AnInquiringMind
5

Jeśli dobrze zrozumiałem, ten sam obiekt kontaktowy należy do więcej niż jednego obiektu cząsteczkowego, ponieważ reprezentuje pewien rodzaj fizycznego kontaktu między dwiema lub więcej cząsteczkami, prawda?

Pierwszą rzeczą, która moim zdaniem jest wątpliwa, jest to, dlaczego Particlezmienna członkowska jest zmienna std::vector<Contact>? Powinien to być a std::vector<Contact*>lub std::vector<std::shared_ptr<Contact> >zamiast. addContactpowinien mieć inny podpis, jak addContact(Contact *cont)lub addContact(std::shared_ptr<Contact> cont)zamiast.

To sprawia, że ​​dołączanie „Contact.h” w „Particle.h” nie jest konieczne, wystarczy deklaracja class Contact„w. Particle.h” i dołączenie „Contact.h” w „Particle.cpp”.

Następnie pytanie o konstruktora. Chcesz coś takiego

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

Dobrze? Ten projekt jest w porządku, o ile twój program zawsze zna powiązane cząstki w momencie, w którym trzeba utworzyć obiekt kontaktowy.

Uwaga: jeśli wybierzesz std::vector<Contact*>trasę, musisz zainwestować kilka przemyśleń na temat żywotności i własności Contactobiektów. Żadna cząstka nie jest właścicielem swoich kontaktów, kontakt prawdopodobnie będzie musiał zostać usunięty tylko wtedy, gdy oba powiązane Particleobiekty zostaną zniszczone. Użycie std::shared_ptr<Contact>zamiast tego automatycznie rozwiąże ten problem. Albo pozwalasz obiektowi „otaczającemu kontekstowi” przejąć własność cząstek i kontaktów (jak sugeruje @rwong) i zarządzać ich żywotnością.

Doktor Brown
źródło
Nie widzę korzyści z addContact(const std::shared_ptr<Contact> &cont)ponad addContact(std::shared_ptr<Contact> cont)?
Caleth,
@Caleth: zostało to omówione tutaj: stackoverflow.com/questions/3310737/... - „const” nie jest tu tak naprawdę ważne, ale przekazywanie obiektów przez referencję (i skalary według wartości) jest standardowym idiomem w C ++.
Doc Brown
2
Wiele z tych odpowiedzi wydaje się pochodzić ze wstępnego moveparadygmatu
Caleth z
@Caleth: ok, aby zadowolić wszystkich nitpickerów, zmieniłem tę dość nieistotną część mojej odpowiedzi.
Doc Brown,
1
@PhysicsCodingEnthusiast: nie, chodzi przede wszystkim o stworzenie particle1.getContacts()i particle2.getContacts()dostarczenie tego samego Contactobiektu reprezentującego fizyczny kontakt między particle1i particle2, a nie dwa różne obiekty. Oczywiście można spróbować zaprojektować system w taki sposób, że nie ma znaczenia, czy Contactw tym samym czasie dostępne są dwa obiekty reprezentujące ten sam kontakt fizyczny. Wymagałoby to uczynienia Contactniezmiennego, ale czy jesteś pewien, że tego właśnie chcesz?
Doc Brown
0

Tak, to, co opisujesz, jest bardzo akceptowalnym sposobem upewnienia się, że każda Contactinstancja znajduje się na liście kontaktów Particle.

Bart van Ingen Schenau
źródło
Dzięki za odpowiedzi. Czytałem kilka sugestii, że należy unikać pary wzajemnie zależnych klas (na przykład w „Wzorcach projektowych C ++ i wycenie instrumentów pochodnych” autorstwa MS Joshi), ale najwyraźniej niekoniecznie jest to poprawne? Czy z ciekawości może istnieć inny sposób wdrożenia tej automatycznej aktualizacji bez konieczności współzależności?
AnInquiringMind
4
@PhysicsCodingEnthusiast: Posiadanie współzależnych klas stwarza różnego rodzaju trudności i powinieneś ich unikać. Ale czasami dwie klasy są tak ściśle ze sobą powiązane, że usunięcie współzależności między nimi powoduje więcej problemów niż sama współzależność.
Bart van Ingen Schenau
0

To, co zrobiłeś, jest poprawne.

Inny sposób ... Jeśli celem jest upewnienie się, że każdy Contactznajduje się na liście, możesz:

  • tworzenie bloków Contact(konstruktorów prywatnych),
  • przekaż Particleklasę,
  • uczyń Particleklasę przyjacielem Contact,
  • w Particletworzeniu metody fabrycznej, która tworzyContact

Więc nie musisz się particle.hw to włączaćcontact

Robert Andrzejuk
źródło
Dzięki za odpowiedzi! To wydaje się być użytecznym sposobem na wdrożenie tego. Zastanawiam się, po mojej edycji pierwszego pytania dotyczącego Networkklasy, czy to zmienia sugerowaną strukturę, czy nadal będzie takie samo?
AnInquiringMind,
Po zaktualizowaniu pytania zmienia się zakres. ... Teraz pytasz o architekturę swojej aplikacji, wcześniej była to kwestia techniczna.
Robert Andrzejuk,
0

Inną opcją, którą możesz rozważyć, jest utworzenie konstruktora Contact, który akceptuje szablon Particle referencyjny. Umożliwi to kontaktowi dodanie się do dowolnego implementowanego kontenera addContact(Contact).

template<class Container>
Contact(/*parameters*/, Container& container)
{
  container.addContact(*this);
}
Błędny
źródło