Współdziałanie klas

Współdziałanie klas
CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
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.

AK
  • Rejestracja:ponad 6 lat
  • Ostatnio:8 dni
  • Postów:3561
2

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

Kopiuj
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

Kopiuj

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.


Bo C to najlepszy język, każdy uczeń ci to powie
edytowany 3x, ostatnio: AnyKtokolwiek
CR
Oczywiście gdzie mogę, tam używam forward declaration, ale tak jak piszesz, dostęp do pól wymaga pełnej deklaracji, a u mnie taki właśnie dostęp jest potrzebny ;/.
AK
To się zawsze udawało uporządkować. Oczywiście nie widząc projektu, nie pomogę.
CR
Coś pokombinuję jeszcze, ale tak czy inaczej - dzięki!
TomaszLiMoon
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 2 godziny
  • Postów:530
0

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

Kopiuj
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ć

Kopiuj
 Locator::provide(this);

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

Kopiuj
auto engine = Locator::getEngine();
Patryk27
mmm singleton i ukryte zależności - tak, tego właśnie potrzeba
AK
Jak to się wzorce wynaturzyły. Wystarczy wzorze nisko oceniany / antywzorzec nazwać inaczej, i wyznawcy łykną ...
CR
Nie, singletona nie chcę, nie ważne jak go nazwiemy :). Niemniej dzięki za sugestię.
TomaszLiMoon
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 2 godziny
  • Postów:530
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.

Kopiuj
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};
};
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
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.


edytowany 6x, ostatnio: Patryk27
TomaszLiMoon
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 2 godziny
  • Postów:530
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.

Patryk27
Coby nie spamować głównego wątku: uważam, że wykorzystywanie zmiennych globalnych (a zatem również singletonów) jest bolesne, ponieważ chowa zależności i przenosi "wiring" ze statycznego, compile-time konstruktora do dynamicznego runtime'u - oznacza to, że uprzednio działająca aplikacja może z dnia na dzień odmówić posłuszeństwa, gdy np. klasa B wymagana przez klasę C nie zostanie odpowiednio wcześnie zarejestrowana przez klasę A, bo użytkownik np. wyłączył audio.
Patryk27
W przypadku podejścia ze statycznym wiringiem (tj. ręcznie wstrzykiwanymi zależnościami) coś takiego wyszłoby na etapie pisania kodu, lecz w przypadku zmiennych globalnych jesteśmy skazani na sprawdzanie ręcznie, poprzez uruchamianie aplikacji w różnych konfiguracjach. Dla mnie tego typu zależności są trudne w odkryciu, trudne w debugowaniu i w zasadzie poza pewną wygodą pisania nie dostrzegam żadnych zalet oraz unikam gdzie tylko się da.
RI
  • Rejestracja:około 5 lat
  • Ostatnio:prawie 5 lat
  • Postów:40
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:

Kopiuj
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:

Kopiuj
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.

CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
0

@Riki:

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

Kopiuj
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?

edytowany 10x, ostatnio: Crow
RI
  • Rejestracja:około 5 lat
  • Ostatnio:prawie 5 lat
  • Postów:40
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ć.

Kopiuj
//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.

CR
  • Rejestracja:ponad 16 lat
  • Ostatnio:11 miesięcy
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:

Kopiuj

//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?

edytowany 7x, ostatnio: Crow
RI
Odpowiedź masz w poście powyżej. Wszystko się zgadza. Dla Twojego rozwiązania deklarujesz WindowProc w Events.
CR
@Riki: Jeżeli dobrze rozumiem, sugerujesz, że powinienem zrobić osobną klasę Events (siedzącą w Engine?), umieścić w niej WndProc, a potem podać jego referencję MainWindow za pomocą osobnej funkcji?
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)