Autor Wątek: Zawieranie się obiektów  (Przeczytany 10341 razy)

Offline Xion

  • Redaktor
    • xion.log

# Sierpień 02, 2007, 20:48:04
Od pewnego czasu zastanawiam się nad rozwiązaniem pewnej kwestii związanej z organizacją interfejsu dla pewnego dość powszechnie występującego połączenia klas. Oto rysunek:



W skrócie wygląda to tak, że Container zawiera w sobie dowolną ilość różnych Elementów, wyprowadzonych z jednej klasy bazowej. Każdy Element ponadto należy do co najwyżej jednego Containera i zawsze wie do którego.

To nad czym się zastanawiam, to sposób na realizację tworzenia elementów i dodawania ich do kontenera. Widzę takie oto opcje:

1) Container ma metody dla każdego typu elementów: AddElementA(), itd., które przekazują podane im parametry do konstruktorów elementów.
2) Container ma tylko jedną metodę Add, która modyfikuje podany jej "wolny" element (z pustym polem Owner) tak, by jego pole Owner wskazywało na kontener.
3) Container znów ma tylko jedną metodę Add, ale ona tylko dodaje element bez jego modyfikacji. Metoda ta jest natomiast wywoływana przez konstruktor elementu.

Naturalnie pytanie brzmi: która jest najlepsza pod względem elegancji, rozszerzalności i łatwości/trudności popełnienia błędów (w tej kolejności)? Jakiś czas temu Regedit przekonał mnie do trzeciej, lecz obecnie sam skłaniam się raczej ku drugiej.

Ciekaw więc jestem waszych opinii, gdyż chciałbym zdecydować się na którejś z tych rozwiązań, jako że zamierzam niedługo wziąć się za pisanie systemu GUI, a potem za zarządzanie sceną. I oczywiście w obu tych modułach wystąpi opisany wyżej model.

Cały temat jest nieco obszerniej opisane w notce na moim devlogu: Trzy rozwiązania dla relacji zawierania.

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Sierpień 02, 2007, 20:55:55
Wybieram bramkę numer 2. :) Powody: mało kodowania (nie trzeba osobnych metod) i łatwość użycia (możesz stworzyć element, a dopiero potem go wrzucić, a nawet mogą istnieć elementy nie dodane do żadnego kontenera).

Offline Grelo

  • Użytkownik

# Sierpień 02, 2007, 21:02:54
Moim zdaniem (chociaż możecie powiedzieć , że się nie znam bo nie ma doświadczenia w pisaniu kontenerów) najbardziej logicznie wygląda metoda 3 bo można najpierw zadeklarować sobie element a dopiero później dodać go bez zmieniania. Myślę, że będzie też łatwiejsza do zakodowania niż dwie pozostałe. Jest ona w miarę możliwości elegancka, ale chyba mało elastyczna.

Offline Xion

  • Redaktor
    • xion.log

# Sierpień 02, 2007, 21:21:12
Wybieram bramkę numer 2. :) Powody: mało kodowania (nie trzeba osobnych metod) i łatwość użycia (możesz stworzyć element, a dopiero potem go wrzucić, a nawet mogą istnieć elementy nie dodane do żadnego kontenera).
Spora część problemu sprowadza się wg mnie właśnie do tego, czy dopuszczamy wolne elementy czy nie. W rozwiązaniu drugim jest to jak najbardziej możliwe; dla mnie jest to zaletą, dla niektórych może być wadą (łatwość popełnienia "błędu" pt. stworzyłem element, ale go nie dodałem).

Grelo: W metodzie 3 Container::Add() jest wywoływana przez konstruktor elementu:
Cytat: Notka z devloga
// konstruktor
Element::Element(Container* pCont, parametry) : m_pCont(pCont)
{
if (pCont) pCont->AddElement (this);
}
więc zasadniczo możliwa jest taka konstrukcja:
new ElementA(pCont, ...);która jest jak najbardziej poprawna (tworzy element i go dodaje, jest on potem dostępny przez interfejs pojemnika), ale wygląda jak wyciek pamięci. I to IMHO eleganckie ani odporne nie jest.

Offline Kurak

  • Użytkownik

# Sierpień 02, 2007, 21:32:14
AFAIK np. w Irrlichcie jest stosowane 3 (parametr konstruktora "parent" i dodawanie do listy dzieci rodzica), ale możliwe także ustalenie rodzica w innym miejscu (np. addChild(dziecko) czy też setParent(rodzic))). Ja skłaniam się do stosowania zarówno 2, jak i 3, bo czasem przydatna jest możliwość zmiany rodzica.

Offline Liosan

  • Moderator

# Sierpień 02, 2007, 23:20:28
Ja bym optował za 2. Żeby "uelegancić" kwestie wolnych obiektów można zastosować kontener wirtualny - obiekt "brak kontenera". Mógłby on być podklasą kontenera, najlepiej singletonem lub monostate'em. To tylko taki pomysł :)

Liosan

PS "brak kontenera" jako podklasa kontenera to naprawdę czysty wymysł...

Offline Xion

  • Redaktor
    • xion.log

# Sierpień 02, 2007, 23:41:54
Kurak: Opcja 2 nie uniemożliwia zmiany rodzica - wystarczy usunąć element z jednego kontenera i dodać do drugiego. A z posiadaniem i SetParent i AddChild wiąże się ten problem, że w sumie do dodania elementu trzebaby użyć ich obu, co nie jest wygodne.

Liosan: IMHO zwykły wskaźnik pusty załatwia sprawę :)

Offline Kurak

  • Użytkownik

# Sierpień 02, 2007, 23:46:09
Kurak: Opcja 2 nie uniemożliwia zmiany rodzica - wystarczy usunąć element z jednego kontenera i dodać do drugiego. A z posiadaniem i SetParent i AddChild wiąże się ten problem, że w sumie do dodania elementu trzebaby użyć ich obu, co nie jest wygodne.
Nie, nie trzeba. SetParent wywołuje AddChild rodzica, natomiast AddChild dodaje wskaźnik na dziecko do listy dzieci i ustawia u dziecka wskaźnik na siebie jako rodzica. Wywołanie jakiejkolwiek z tych funkcji automatycznie powoduje zerwanie więzi z innym rodzicem.

Offline Xion

  • Redaktor
    • xion.log

# Sierpień 03, 2007, 00:18:11
SetParent() musiałby jeszcze wywołać RemoveChild() starego rodzica, a potem AddChild() nowego. W sumie więc dodanie tej funkcji robi nam jakiś miks modelu 2 z 3, ze wskazaniem na 3.
I szczerze mówiąc nie bardzo uśmiecha mi się robienie tego SetParent, którego implementacja zakłada że elementy manipulują kontenerem. Po prostu bardziej odpowiada mi to, że obiekty są "bierne", a to interfejs kontenera manipuluje ich przynależnością.

Zawsze jednak jestem otwarty na sugestie, że może to "aktywne" są lepszym rozwiązaniem :)

RageX

  • Gość
# Sierpień 03, 2007, 01:12:55
Każdy Element ponadto należy do co najwyżej jednego Containera i zawsze wie do którego.
IMO, dobrze by było żeby container tez wiedział (zakładamy że container-y są dużymi, nisko dynamicznymi obiektami), co ma na składzie... właściwie ja bym sobie odpuścił świadomość elementu o przynależności do kontenera. Enkapsulacja.
1) Container ma metody dla każdego typu elementów: AddElementA(), itd., które przekazują podane im parametry do konstruktorów elementów.
2) Container ma tylko jedną metodę Add, która modyfikuje podany jej "wolny" element (z pustym polem Owner) tak, by jego pole Owner wskazywało na kontener.
3) Container znów ma tylko jedną metodę Add, ale ona tylko dodaje element bez jego modyfikacji. Metoda ta jest natomiast wywoływana przez konstruktor elementu.
jesli już muszę wybierać z tych tutaj to nr2... ale odpuścił bym sobie informowanie elementów o przynależności. Zazwyczaj elementy wywołujesz przez kontener, a nie kontener przez elementy. Od ogółu do szczegółu w tym przypadku... IMO of kors naturliś jawol.
Edit: Oraz odpuścił bym sobie "uniwersalne" elementy. Kontener mógłby wtedy przyjmować tylko przygotowane dane, tylko w wymaganej formie.
Efekt - czystszy, szybszy, trochę większy kod.

No jeszcze dodam, że jestem przeciwny składowaniu elementów bez właścicieli. W układzie który preferuje w tym przypadku (kontenery odpowiadają za swoje elementy - despotyzm) elementy bezpańskie są bezużyteczne.
« Ostatnia zmiana: Sierpień 03, 2007, 01:22:31 wysłana przez RageX »

Offline Reg

  • Administrator
    • Adam Sawicki - Home Page

# Sierpień 03, 2007, 16:05:24
Też się nad tym problemem dużo zastanawiam. Napisałem też swój tekst nawiązujący do notki z blogu Xiona:

http://regedit.warsztat.gd/Download/Rozne/Rozwiazania%20dla%20relacji%20zawierania.html

Zawarłem w nim też drugi, pokrewny problem - jak identyfikować obiekty. Czy przez wskaźniki, czy przez identyfikatory liczbowe, czy może przez nazwy łańcuchowe.

Co do problemu dodawania obiektów to ja do niedawna byłem zwolennikiem rozwiązania 2, ale teraz wolę rozwiązanie 3. Jeśli obiekt sam dodaje się do konterena w konstruktorze, a usuwa w destruktorze (a kontener też zapewne w destruktorze usuwa swoje elementy), dziwnie może wyglądać:
new Element(1, 2, 3);Zupełnie jak wyciek pamięci. Ale lepsze to, niż gdyby trzeba było jeszcze wywołać Kontener.Add, co stwarza okazję żeby o tym zapomnieć i zrobić prawdziwy wyciek pamięci tydzież inny paskudny błąd.

RageX

  • Gość
# Sierpień 03, 2007, 17:39:38
Zawarłem w nim też drugi, pokrewny problem - jak identyfikować obiekty. Czy przez wskaźniki, czy przez identyfikatory liczbowe, czy może przez nazwy łańcuchowe.
Heh, widzę że to nie tylko mój problem, cieszy mnie to. Sprawa wygląda tak, z jednej strony potrzebujesz jakoś "filtrować" dane, ,a z drugiej, jakoś poręcznie się odwoływac do poszczególnych elementów, a z trzeciej dalej zachować te "minimalne" rozmiary elementów. Mi, w C# z pomocą przyszedł słownik + enumy. Pamiętam że trochę nad tym spędziłem (sprawdzałem różne sposoby i ich wydajność, przydatność). Wcześniej używałem samych indeksów(tablic, list) i enumy castowałem do int-ów, tez nie głupie to było i minimalnie szybsze, niż aktualne rozwiązanie.
pozdrawiam :D

Offline Rolnix

  • Użytkownik

# Sierpień 03, 2007, 20:09:36
Nie wiem czy zbliżę się do sedna sprawy, ale czy nie mógłbyś za pomocą uniwersalnego Add() dodawać wskaźników bazowego typu, a potem używać dynamic_cast<>?

Jeżeli nie chcesz trzymać dużych ilości elementów, identyfikacja może być rozwiązana przez łańcuchy znaków... ewentualnie potem, pod koniec testów gry/programu - kiedy urośnie Ci ilość elementów - możesz wygenerować unikalne identyfikatory liczbowe dla każdego z nich.
« Ostatnia zmiana: Sierpień 03, 2007, 20:12:25 wysłana przez Rolnix »

RageX

  • Gość
# Sierpień 03, 2007, 20:22:30
identyfikacja może być rozwiązana przez łańcuchy znaków... ewentualnie potem, pod koniec testów gry/programu - kiedy urośnie Ci ilość elementów - możesz wygenerować unikalne identyfikatory liczbowe dla każdego z nich.
No właśnie byś popełnił ten mały błąd, łańcuchy znaków są do kitu jesli idzie o filtrowanie kolekcji. Tak więc możesz dodać taki identyfikator, ale i tak byś musiał na podstawie innego, kolejnego już identyfikatora filtrować elementy.

Offline Rolnix

  • Użytkownik

# Sierpień 03, 2007, 20:26:36
Albo pierwsza litera identyfikatora (nawet dynamicznie dodawana) oznaczałaby typ kolekcji ;). "M" - model, "T" - tekstura itd...