Trzy rozwiązania dla relacji zawierania

2007-07-25 13:47

Chcę dzisiaj zająć się pewną sprawą natury projektowej. Mam tu mianowicie na myśli sytuację, gdy mamy do czynienia ze zbiorem różnych elementów, wywodzących się z tej samej klasy bazowej. Najlepiej widać to będzie na odpowiednim diagramie:

Relacja zawierania

 

Jest tu klasa kontenera (Container), która posiada m.in. zestaw różnych Elementów. Ich dokładne typy nie są i nie muszą być znane – ważne, że wywodzą się od klasy bazowej Element. W szczególności klasy Container i Element mogą być jednym i tym samym – wtedy będziemy mieli do czynienia ze strukturą hierarchiczną, reprezentującą np. drzewo węzłów dokumentu XML czy kontrolki systemu GUI. Zauważmy jeszcze, że każdy element wie o swoim właścicielu (pole Owner), czyli o pojemniku, który go zawiera (albo o elemencie nadrzędnym w przypadku drzewa).
Z obsługą już dodanych elementów nie ma zbytniego problemu, jako że wyjątkowo naturalne jest tu wykorzystanie metod wirtualnych i polimorfizmu. Kłopot polega właśnie na tworzeniu i dodawaniu nowych elementów – a ściślej na interfejsie (funkcjach i parametrach), które mają do tego służyć. I tutaj właśnie pojawiają się te trzy rozwiązania.

Rozwiązanie pierwsze polega na wyposażeniu klasy Container w garnitur metod zajmujących się tworzeniem każdego typu elementów i dodawaniem ich do zbioru. W naszym przypadku byłyby to więc funkcje AddElementA, AddElementB i AddElementC:

  1. void Container::AddElementA(parametry)
  2. {
  3. Elements.insert (new ElementA(this /*właściciel nowego elementu */, parametry);
  4. }

Jak widać w tym przypadku dobrze jest wyposażyć konstruktory klasy Element i pochodnych w parametr, przez który będziemy podawali wskaźnik do nadrzędnego kontenera lub elementu.

Drugie rozwiązanie zakłada, że pojemnik dysponuje tylko jedną ogólną metodą AddElement. Przekazujemy jej już utworzone obiekty, a jej zadaniem jest tylko dodać je do już posiadanych:

  1. Container->AddElement (new ElementA(parametry));

Zauważmy, że tutaj konstruktory elementów już nie przyjmują wskaźników na swoich właścicieli. Dzięki temu możemy uchronić się przed omyłkowym dodaniem elementu do innego pojemnika niż ten, który podaliśmy podczas jego tworzenia.
Skąd jednak element wie, do kogo należy?… Za to odpowiada już metoda dodająca, która modyfikuje pole określające właściciela:

  1. void Container::AddElement (Element* pElem)
  2. {
  3. Elements.insert (pElem);
  4. pElem->m_pOwner = this;
  5. }

Oczywiście w tym celu klasa Container musi być zaprzyjaźniona z klasą bazową Element, co na szczęście w C++ nie stanowi problemu :) (W innych językach, jak np. C# czy Java, można osiągnąć bardzo podobny efekt przy pomocy pakietów.)

W końcu trzecie wyjście przenosi cały ciężar pracy na same elementy. W tej koncepcji to elementy same się dodają do podanego pojemnika:

  1. // konstruktor
  2. Element::Element(Container* pCont, parametry) : m_pCont(pCont)
  3. {
  4. if (pCont) pCont->AddElement (this);
  5. }

Żeby miało to sens, muszą być one aczkolwiek tworzone dynamicznie poprzez operator new.

Czas na nieodparcie nasuwające się pytanie, czyli… które rozwiązanie jest najlepsze? No cóż, sprawa nie jest taka prosta (inaczej bym o niej nie pisał ;D), więc może przyjrzyjmy się kilku argumentom:

  • Rozwiązanie nr 1 jest dość powszechnie stosowane zwłaszcza tam, gdy elementy mają ustalone i raczej niezmienne typy. Przykładem jest wspomniane drzewko węzłów XML: tam wachlarz typów ogranicza się do tekstu, elementów XML, atrybutów, sekcji CDATA, instrukcji przetwarzania i jeszcze paru innych. Napisanie dla każdego z nich odpowiedniej metody Add* nie jest specjalnie czasochłonne.
  • Trzecie rozwiązanie jest najwygodniejsze i daje najmniejsze pole manewru, jeżeli chodzi o popełnienie błędów – np. stworzenie obiektu i niedodanie go do pojemnika. Z drugiej strony może ono produkować nieintuicyjny kod. Jeżeli bowiem chcemy tylko dodać jakiś element i nie jest on nam za chwilę potrzebny, to użyjemy poniższego kodu:
    1. new ElementA(pContainer, parametry);

    Jest więc new, którego rezultat ignorujemy i tylko po dłuższym przyjrzeniu się można zauważyć, że to jednak nie jest wyciek pamięci.

  • Zarówno trzecie, jak i (zwłaszcza) drugie rozwiązanie dopuszcza tworzenie elementów “wolnych”, nieprzypisanych do żadnego pojemnika. Wbrew temu co napisałem przed chwilą, taka czynność nie musi być wcale błędem. W takim przypadku odwołanie do właściciela w elemencie ustawiamy po prostu na NULL.

Cóż więc wybrać? Ostatnio skłaniam się ku drugiemu rozwiązaniu. Według mnie jest ono z tych trzech najbardziej przejrzyste – tworzenie obiektu to jedno (operator new), a jego dodawanie drugie (metoda Add). Ponadto dzieli ono pracę w miarę równo między element, jak i pojemnik – w przeciwieństwie do pozostałych modeli. I w końcu, nic w tym rozwiązaniu nie jest robione za plecami programisty, co oczywiście może być zaletą, ale i wadą oznaczającą, że przecież wszystko musimy zrobić samemu.

A zatem… kwadratura koła? W takich sytuacjach warto chyba iść na kompromis, czyli poprzeć prostokąt z zaokrąglonymi rogami ;)

Be Sociable, Share!
Be Sociable, Share!
Tags: , ,
Author: Xion, posted under Programming »


2 comments for post “Trzy rozwiązania dla relacji zawierania”.
  1. Reg:
    July 25th, 2007 o 14:03

    Ciekawa notka! Mnie też nurtuje ten problem i też dochodzę do podobnych wniosków. Dodałbym tutaj:

    – Jeśli jest wiele kontenerów, model 3 nadal może działać – tworzonemu elementowi trzeba wtedy do konstruktora podać kontener, do którego on ma należeć.

    – Problem zwalniania – czy użytkownik elementów może ich nie zwolnić przez zwolnieniem kontenera i kontener sam je sobie zwolni?

    – Inne jeszcze modele tworzenia elementów, np. taki jaki nadal pokutuje w mojej (starej już :) szóstej wersji silnika:

    struct MATERIAL { … };
    class MaterialCollection
    {
    public:
    // Tworzy lub uaktualnia
    void SetMaterial(string Name, MATERIAL Params);
    // Usuwa
    void DeleteMaterial(string Name);
    };

  2. Xion:
    July 25th, 2007 o 18:08

    – No tak, to nie jest żadne odkrycie – patrz ostatni sampel kodu :)

    – Rzeczywiście nie napisałem tego, chociaż to pośrednio wynikało z użycia określenia “właściciel”. Tak, kontener zwalnia dodane do niego obiekty – m.in. po to je grupujemy :]

    – Ciekawe :) Ale to wariacja na temat sposobu 1. Poza tym taka kolekcja obejmuje obiekty tylko jednego typu więc nie ma tu problemu rozrastającego się interfejsu.

Comments are disabled.
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.