[C++] Wywoływanie metody "z rodzica" obiektu


(Marcinch7) #1

Hej, mam problem związany z obiektowością w C++, zawsze wolałem programowanie proceduralne i teraz się to na mnie zemściło :smiley:

Powiedzmy że mam dwie klasy, CGame i CObject:

class CGame {

public:

 CObject obiekt;

 InnaKlasa cokolwiek;

...

};

a w instancji obiekt klasy CObject potrzebuje wywołać metodę znajdującą się w instancji klasy CGame czyli powiedzmy:

class CObject {

public:

 void render() {

  ...

  (instancja_klasy_cgame).metoda();

  ...

 }

...

};

W sumie to mógłbym użyć singletonu, ale czy jest sens? Proszę o opinie.


(Fiołek) #2

Singleton(czy lepiej zmienna globalna) albo przekazywanie wskaźnika na CGame w konstruktorze(albo jakiejś metodzie) CObject.


(Wojtekbogocki) #3

A ile masz instancji CGame? Jeśli jeden to najsensowniejsze w takich przypadkach są singletony, jeśli kilka to można przekazywać wskaźnik do macierzystego obiektu CGame w konstruktorze CObject czyli np przy tworzeniu nowej instancji klasy CObject:

CObject instancja(CGame* gameInst);

ale skoro robisz obiektówkę i instancje klasy CObject tworzysz statycznie to należałoby to zrobić w konstruktorze CGame:

CGame::CGame() : instancjaObj(this) {}

:slight_smile:

edit.

Tja jak zwykle uprzedzony... @Fiołek, jeśli nie masz nic przeciwko zachowam post :slight_smile:


(Enterbios) #4

Ja bym raczej polecił się zastanowić czy dobrze klasy zaprojektowałeś, o ile nie musisz czarować bo używasz czyjegoś kodu to z doświadczenia wiem że takie rzeczy można śmiało obejść poprzez poprawne zaprojektowanie klas. Oczywiście można to obejść "szybciej" ale to już musisz sobie zadać pytanie czy będziesz chciał później z tym kodem pracować i go rozwijać. Jeśli tak to przeprojektuj klasy, jeśli nie to i tak przeprojektuj klasy będziesz miał doświadczenie na przyszłość.


(Marcinch7) #5

To nie jest czyjś kod, jest on mój. Inaczej (chyba) nie da się tego zaprojektować a jest to wina używanej biblioteki.

Sprawa wygląda tak, mam powiedzmy klasę Window, z pewnej biblioteki i klasę CGame który dziedziczy z tej klasy. W klasie CGame mam instancje obiektu CObject, i w którymś miejscu w tym obiekcie muszę wywołać metodę "rodzica" (w cudzysłowiu, bo z dziedziczeniem niema on nic wspólnego) czyli powiedzmy Render(), którą klasa CGame dziedziczy z klasy Window.


(Enterbios) #6

Nie jestem obiektowym purystą ale skoro z CObject potrzebujesz wywołać metodę z CGame to nie próbujesz przypadkiem wykonać tam kodu który w gruncie rzeczy powinna zrobić klasa CGame? Jeśli jesteś pewien że to CObject powinna się zająć tym kodem i musi ona wywołać metodę z CGame to masz racje ale pod warunkiem że to CGame będzie składnikiem klasy CObject. Tak dla założenia że jednak masz racje i klasa CObject wykonuje to zadanie które powinna, ale jednak "musi" wywołać metodę z klasy CGame której jest składnikiem. To co jeśli zrobisz tak:

przykladowo:

funkcja_klasy_CObject()

{

instrukcje1;

wywolanie_metody_z_CGame;

instrukcje2;

}

zastępujesz to dwiema metodami, w pierwszej masz instrukcje1 w drugiej instrukcje2, wtedy w klasie CGame piszesz metodę która zrobi tak:

funkcja_klasy_CGame()

{

cobject.instrukcje1();

metoda_CGame();

cobject.instrukcje2();

}

(Marcinch7) #7

Poradziłem sobie - singleton.

CGame::GetInstance().Draw(a,b,c);

(Fiołek) #8

@ EnterBioS : nie do końca tak się da w tym wypadku. Nazwy, które używa Marcin1147 i to, co chce zrobić, wskazuje, że klasa CGame jest rendererem(albo zawiera w sobie renderer) a CObject - obiektami gry. Tutaj, z tego co się zdążyłem "nauczyć", nie ma złotego środka. Kod jest albo mało obiektowy(enkapsulacja jest na niskim poziomie) i trudny w rozbudowie - obiekty gry rysują się same, ale muszą się jakoś narysować, czyli muszą odwołać się do renderera - albo robimy wszystko "tak jak być powinno", czyli rozdzielamy to, co rysuje, to co zarządza tym co jest rysowane itp. Takie rozwiązanie jest o wiele lepsze, ale ciągnie za sobą duży narzut na rozdzielenie wszystkiego. Nie zawsze się opłaca(więcej: rzadko się opłaca to robić, jeśli wcześniej czegoś takiego nie mieliśmy a tworzymy mały projekt).

@autor: jeśli chcesz to ładnie rozwiązać, rozdziel renderer(okno z metodą(ami) Draw) od obiektów(np. wróg, gracz), managera obiektów(zwykła lista obiektów i w pętli ich akutalizacja/rysowanie) i samej gry(tworzeniy i zarządza oknem(zdarzenia), obiektami(odrysowywanie, aktualizacja) itp.). Nie będzie to rozwiązanie idealne, ale o wiele łatwiej Ci będzie taki kod utrzymać(i nie będzie zależności od "rodzica", choć takie nie są tak naprawdę złe, jeśli używamy ich z rozwagą).

Taki mały spam: kiedyś opisywałem, jak w miarę dobrze(DOD-ready, enkapsulacja, używają takich systemów niektóre gry AAA) zbudować silnik gry(m.in. i renderer) - posty "Trochę o architekturze silnika". Nie jest to rozwiązanie idealne(trochę przesadziłem ze złożonością, można to prościej rozwiązać), ale daje jakiś zarys


(Enterbios) #9

Jak coś robić to zawsze dobrze, imho nie ma sensu pisać "brzydkiego kodu", w czymkolwiek z czym chcemy spędzić trochę czasu. Jedyna opcja kiedy pozwalam sobie na pisanie nie "clean code" to kiedy skrobie coś dla zabawy, dla sprawdzenia możliwości biblioteki. Po twoim blogu widzę że masz trochę doświadczenia więc zapewnię dobrze wiesz co będzie czekać naszego kolegę w tym projekcie za jakiś czas, kiedy się okaże że klasa pojazd musi mieć dostęp do obliczeń fizycznych, danych całej mapy i obiektów zajmujących się animacja. Oczywiście można sobie tutaj poradzić singeltonem ale to już jest "bad smelling code" i prędzej czy później projekt poleci do kosza. Sam tak miałem ze swoim pierwszym projektem tego typu, po 2miesiącach trzeba było silnik przepisać od nowa.

Polecam książkę "Clean code" Roberta C. Martina, mimo że przy pierwszym czytaniu wydaje się kontrowersyjna tak moim (i nie tylko) zdaniem przy większych projektach jest nie zastąpiona, a na pewno potrafi zaoszczędzić czas na pytania "Jak wywołać metodę z "rodzica" obiektu" oraz sporo czasu straconego na motanie się po własnym kodzie.


(Fiołek) #10

Wszystko zależy od wielkości projektu. Wydaje mi się, że nie będzie miał wielkich rozmiarów, więc nawet jeśli to będzie "śmierdzący kod", to nie poczuje różnicy, a nie straci masy czasu nad rozmyślenia nad tym jak zaprojektować kod. Dodatkowo, jeśli projekt nie jest rozbudowany, to taka "poprawna" architektura będzie zwykłym code bloat, który dodatkowo może przeszkadzać(vide jeden z moich projektów, choć tam miałem kilka znacznych błędów projektowych), a gdy projekt zacznie się rozrastać to sam zauważy, że coś jest nie tak i zacznie robić to poprawnie oraz będzie miał nauczkę, jak nie pisać i jak dobierać rozwiązania do projektu.

Już któraś osoba poleca mi tą książkę, muszę w końcu przeczytać. Dzięki!


(Marcinch7) #11

Ok, Fiołek widać że trochę już w tym siedzisz. Zawsze wolałem wszystko robić proceduralnie i teraz się to mści bo przy "większym" projekcie łatwo jest się pogubić. Zrobiłem to tak jak mówiłeś - rozdzieliłem renderer od obiektów i wszystko działa super. Co do projektu, to nie jest nic "komercyjnego" ot prosta gra mająca na celu poduczenie mnie języka. Ogółem wszystko wygląda tak: główna klasa SGame, renderer SManager. Każda klasa obiektu w grze dziedziczy z klasy CObject pokrywając jej wirtualne metody: update, render itd. Po wywołaniu domyślnego konstruktora CObject, obiekt dodaje się do listy SManagera i jest renderowany i odświeżany 60 klatek na sekundę.


(Fiołek) #12

Dobry kod proceduralny będzie tak samo dobry jak dobry kod obiektowy, więc nic nie stoi na przeszkodzie by to proceduralnie napisać(co prawda dobrze znać i ten paradygmat).

To jeszcze usuń domyślny konstruktor CObject(chyba, że naprawdę wiesz co robisz) na rzecz przekazywania wskaźnika na SManager w osobny sposób(np. zwykłego przypisywania do zmiennej, np. CObject::Renderer, w metodzie SManager(np. AddObject) dodającej do niej CObject), nie rób SManager singletonem i jesteś na dobrej drodze do ładnie obiektowego(ale niekoniecznie odpowiedniego do gry - ale do tego sam kiedyś dojdziesz) kodu :wink:


(Marcinch7) #13

Konstruktor w SObject nie ma nic wspólnego z wskaźnikiem do SManager - wygląda on tak: SObject(std::string path,float x=0,float y=0); więc wygląda na to że Cię ubiegłem :wink:

SManager też nie jest robiony singletonem więc chyba wszystko jest okej :wink:

Dzięki za pomoc!