Współdziałanie klas

0

Pierwszy raz piszę w C++ większy projekt (do tego w nowym dla mnie środowisku VS 2019) i nie do końca mam pomysł na rozwiązanie pewnego problemu.

Chcę stworzyć silnik graficzny, który będę mógł wykorzystać później do innych projektów. Chciałbym, żeby całość opierała się na jednej klasie abstrakcyjnej Engine, w taki sposób, by praca z silnikiem sprowadzała się do nadpisania kilku funkcji z tej klasy, np. OnKeyDown() albo OnFrameUpdate(). Klasa ta, w swoim konstruktorze, tworzy inne klasy pomocnicze (np. klasę okna), a one tworzą dalsze klasy im podległe i powstaje taka piramidka: Engine --> MainWindow --> Display --> Buffer itd. Co ważne, klasy te w momencie utworzenia przechwytują i przechowują referencje do klasy bazowej Engine, dzięki czemu mają dostęp do siebie nawzajem (są zaprzyjaźnione) i np. MainWindow może w swojej funkcji obsługi komunikatów (WndProc), wykorzystywać funkcje, które znajdują się bezpośrednio w Engine (jak chociażby abstrakcyjne OnKeyDown(), które może być wykonywanie w momencie odebrania komunikatu WM_KEYDOWN). Mógłbym taką funkcję OnKeyDown() umieścić bezpośrednio w klasie MainWindow, ale wtedy praca z silnikiem wymagałaby nadpisania kilku klas, a mi zależy na prostocie.

Wszystko niby OK, tylko z jedną rzeczą nie mogę sobie dać rady. Otóż wszystkie te klasy siedzą w jednym pliku nagłówkowym, bo nie znalazłem sposobu jak trzymać je osobno (rozbijając np. na MainWindow.h i Engine.h) i jednocześnie uniknąć circular reference (by te klasy mogły wzajemnie z siebie korzystać). Da się to zrobić? A może w ogóle zabieram się do tego od niewłaściwej strony i powinienem był to inaczej zaprojektować? Proszę o poradę jak to się powinno zrobić bardziej profesjonalnie.

2

Jednym z trików C++ o jakim warto wiedzieć, jest forward declaration. Pisze się to tak:

class AlaMaKota;

Taki forward może wystąpić wielokrotnie, nie ma z tego powodu konfliktów.

Co czyni legalnym użycie wskaźnika albo referencji tej klasy


void fum(AlaMaKota * wsk); // legalne
AlaMaKota * wsk; // legalne
voif fun(AlaMaKota & ref);   // legalne
// trudno podać przykład na "gołą" referencję, bo musi być zainicjowana, a chyba w takim kontekście nie przejdzie.


AlaMaKota wartość; // nielegalne

Do normalnego użycia (dostępu do pól, deklarowania zmiennej te klasy musi już być znana pełna normalna deklaracja.
Jak można się domyśleć, trik do pewnego stopnia pzowala żyć z cyklicznymi zależnościami.

Oczywiście zabezpieczanie headera pod kątem jednorazowego jego dołączenie, zakładam znasz.

DISCLAIMER: nie wypowiadam się o przedstawionej w poście koncepcji.

0

W tym przypadku można rozważysz użycie wzorca Service Locator.
W najprostszej formie możesz zaprojektować go w taki sposób

class Locator
{
public:
  static Engine* getEngine() { return engine_; } 

  static void provide( Engine* engine )
  {
    engine_ = engine;
  }

private:
  static Engine* engine_;
};

gdzie wystarczy w konstruktorze klasy Engine dodać

 Locator::provide(this);

i z dowolnego miejsca w kodzie będziesz miał do dostęp poprzez:

auto engine = Locator::getEngine();
0

singleton i ukryte zależności - tak, tego właśnie potrzeba

Z technicznego punktu widzenia nie jest to singelton gdyż obiekt klasy Locator można utworzyć wiele razy.
Ale zgadzam się, że w tak podstawowej formie używanie tego wzorca nie jest zalecane z uwagi na jego globalną dostępność w kodzie.

Jednakże poprawnie zastosowany wzorzec Service Locator może być przydatny, nie tylko do udostępniania żądanych danych do klasy, ale także jako pewnego rodzaju filtr nakładany na daną klasę. Filtr ten można tak dostosować aby klasa udostępniła dowolnie skonfigurowane metody lub zmienne.

Rozbudowana wersja wzorca operuje na dziedziczeniu jako formie ograniczenia widoczności klasy Locator, której dodatkowo nie da się użyć w żaden sposób kodzie z poza klasy dziedziczącej lub klasy będącej parametrem szablonowym dla Locator.

template< typename T >
class Locator
{
protected:
  Locator() = default;
  static int scale() { return engine_->scale(); }
  static void provide( T* engine ){ engine_ = engine; }

  friend T;

private:
  static T* engine_;
};

class Engine;

class MainWindow : public Locator<Engine>
{
public:
    void calculateBorder()
    {
        borderWidth *= Locator::scale();
    }
private:
    int borderWidth {1};
};

class Engine
{
public:
    Engine()
    {
        window = make_unique<MainWindow>();
        Locator<Engine>::provide(this);
    }

    int scale() const { return scale_; }
    int getId() const { return id; }

private:
    double scale_ {0.55};
    int id {0};
    unique_ptr<MainWindow> window {nullptr};
};
0

Z technicznego punktu widzenia nie jest to singelton gdyż obiekt klasy Locator można utworzyć wiele razy.

Niech będzie - z technicznego punktu widzenia jest to zatem zmienna globalna :-)

Rozbudowana wersja wzorca operuje na dziedziczeniu jako formie ograniczenia widoczności klasy Locator, której dodatkowo nie da się użyć w żaden sposób kodzie z poza klasy dziedziczącej lub klasy będącej parametrem szablonowym dla Locator.

To w dalszym ciągu jest ten sam wzorzec projektowy, tylko nadmuchany szablonami oraz wymagający - w tym wydaniu - języka z wielokrotnym dziedziczeniem, gdyby chciało się "wstrzyknąć" więcej niż jedną instancję.

Z dwojga złego już wolałem poprzednią wersję - przynajmniej była łatwa w zrozumieniu i nie próbowała kryć się przed byciem singletonem.

0

To w dalszym ciągu jest ten sam wzorzec projektowy, tylko nadmuchany szablonami oraz wymagający - w tym wydaniu - języka z wielokrotnym dziedziczeniem, gdyby chciało się "wstrzyknąć" więcej niż jedną instancję.

"Nadmuchany" jest nie dlatego aby coś ukryć, aby ograniczyć dostępność zawartości klasy Locator do konkretnej klasy. A do wstrzyknięcia więcej niż jednej instancji można zdefiniować klasę Locator jako variadic template i dziedziczyć po Locator<Engine1,Engine2,SoundSystem> itp.

Z dwojga złego już wolałem poprzednią wersję

Myślę że dyskusja byłaby bardziej merytoryczna, gdybyś przedstawił dlaczego w tym konkretnym przypadku użycie powyższego wzorca jest złem.

1

@Crow
U siebie stosuję zazwyczaj koncepcję MVC do prawie każdej aplikacji z Interfejsem:
Model - Dane: listy, mapy, tablice, strumienie, inne dane
Controller - Logika: jakieś operacje
View - GUI
następnie widok dzielę znowu na MVC.
Model - Dane w kontrolkach
Controller - obsługa eventów
View - Formatki, style, układy

Nie wiem co dokładnie chcesz tam uzyskać i po co Tobie jest taki format, ale przekazywanie dziecku referencji do rodzica nie jest dobrym pomysłem.
Jeśli będziesz chciał to przetestować to lekki strzał w stopę.

Warto wykorzystać oklepane wzorce takie jak Budowniczy, Strategia, Dekorator, Fabryka ..., a nie wymyślać coś dziwacznego.
Kod może wyglądać np tak:

class EngineBuilder{
public:
    void setWindow(class WindowType);
    void setLogicToWinow(class WindowLogic);
    void setDisplayToWindow(class Display);
    void setBufferToDisplay(class Buffer);
    Window getEngine() throws exception;
};

class MyWindowLogic : public (Interface WindowLogic){
    void eventSynchronize();
public:
    void OnKeyDown();
    void OnFrameUpdate();
};

Wtedy struktura aplikacji może wyglądać jakoś tak:

class Window{
private: class Display;
}
class Display{
private: class Buffer;
}

W MyWindowLogic zawierasz swoją logikę dla widoku, a jak to sobie połączysz za pomocą metody setLogicToWinow , to już Twój problem.
To jest tylko propozycja.
Nie wiem czy dobrze zrozumiałem Twój problem.

0

@Riki:

Próbuję osiągnąć coś takiego:

class MainWindow
{
    HWND handle;
}

class Display
{
    HDC dc;
}

class Buffer
{
    int size;
    int* color;
    int* depth;
}

class Engine
{
    void OnKeyDown() = 0;
    void OnDraw() = 0;
    void OnResize() = 0;
public:
    MainWindow mainWindow;
    Display display;
    Buffer buffer;
}

No i teraz np. chcę przechwycić DC (Device Contexts) okna, żeby móc później po nim rysować. Dla wygody HDC jest przechowywane w Display (bo to on głównie z niego korzysta), jednak sam HWND, potrzebny do przechwycenia DC, powstaje wewnątrz MainWidow, w chwili wywołania CreateWindow(). Kolejność jest więc taka: Engine tworzy MainWindow, MainWindow wywołuje CreateWindow(), Display (wykorzystując HWND z MainWindow) wywołuje GetDC(), a później po nim rysuje, używając klasy Buffer.

Dodatkowo Engine zawiera szereg abstrakcyjnych eventów (dla wygody zgromadzonych w klasie głównej), z których korzystają wszystkie pozostałe klasy.

Wiem, że mógłbym porobić funkcje, w których klasy mogłyby się powymieniać referencjami do potrzebnych obiektów (już po utworzeniu wszystkich niezbędnych składowych), tylko po co? Co jest złego w podawaniu dziecku referencji do rodzica?

0

Przekazywanie referencji rodzica do dziecka jest złe. Łamiesz zasadę niezależności obiektów. Czyli w uproszczeniu dodajesz niepotrzebne zależności do dziecka.
Ale rób jak uważasz.

Przykład tego jak to widzę.
W Winapi pisałem wieki temu, więc mogę coś źle pamiętać.

//w innym pliku
class MainWindow{
    HWND handle;
    Display display;
    Events events;
    poolMSG();
    //windowProc <- tutaj ?
};

//w innym pliku
class Display{
    HDC dc;
    Buffer buffer;
public:
    Display(HWND handle){getDC(handle);}
    ~Display(){deleteDC();}
};

class Buffer{
    ...
};

//w innym pliku
class Events{ // Klasa wymienna
public:
    void OnKeyDown(handle) = 0;
    void OnDraw(handle) = 0;
    void OnResize(handle) = 0;
    //windowProc <- tutaj ?
};

Jeśli okno jest tworzone w ten sam sposób (Niezmienniki MainWindow,Display,Buffer), a różnica jest w eventach to wystarczy tworzyć egzemplarze klasy Events.

0

@Riki:

Funkcję okienkową WndProc mam jako część MainWindow i to z pewnymi kombinacjami. WndProc nie może być class memberem, bo wtedy zawiera dodatkowy parametr this i struktura WndClass go nie łyknie. Dlatego mam na tę okoliczność osobną statyczną funkcję, którą nazwałem Dispatcherem. Działa ona w ten sposób, że wyciąga z okna zakodowaną wcześniej informację (w tym wypadku wskaźnik na instancję klasy) i przesyła obsługę komunikatów do właściwej funkcji okienkowej (którą nazwałem Handlerem) z tej konkretnej instancji:


//Po stworzeniu okna i pobraniu uchwytu zaszywa w nim wskaźnik do instancji posiadającej je klasy
SetWindowLongPtrW(Handle, GWLP_USERDATA, (LONG_PTR)this);

//Dispatcher:
LRESULT CALLBACK MainWindow::MessageDispatcher(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	MainWindow* Window = reinterpret_cast<MainWindow*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));
	return Window->MessageHandler(hWnd, message, wParam, lParam);
}

//Handler:
LRESULT CALLBACK MainWindow::MessageHandler(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	case WM_PAINT:
	{
		LPPAINTSTRUCT PS{};
		BeginPaint(Handle, PS);
		Parent.Display.DrawFrame(); //Funkcja siedzi w klasie Display. Parent to referencja do klasy rodzicielskiej Engine
		EndPaint(Handle, PS);
		break;
	}
	case WM_ERASEBKGND:
		break;
	case WM_KEYDOWN:
		Parent.OnKeyPress(); //Funkcja związana z wciśnięciem klawisza, siedząca w Engine.
                break;
	default:
		return DefWindowProcW(hWnd, message, wParam, lParam);
	}
}

Eventy - jak wspomniałem - siedzą w głównej klasie Engine i zgodnie z zamysłem, to ona ma być jedyną klasą abstrakcyjną, którą przy implementacji na potrzeby konkretnego projektu, powinno się nadpisać. Jak mam podawać te eventy do funkcji okienkowej MainWindow, nie posiadając z jej poziomu dostępu do Engine? Mógłbym przenieść funkcję okienkową do Engine, ale wtedy jak MainWindow uzyska do niej dostęp bez referencji?

1 użytkowników online, w tym zalogowanych: 0, gości: 1