Modularna klasa, problem z linkerem

0

Mam klasę Graphics i chciałbym aby jej zawartość zależała od użytych nagłówków. Czyli jak np. zrobię #include "2D.h" to w klasie powinna się pojawić funkcja tam zdefiniowana. Póki co mam to zrobione tak:

Graphics.h

struct FrameBuffer; //strukturka zawierająca informacje o uchwycie okna, DC, rozdzielczości itd., jej pełna definicja zalega w nagłówku Display.h.
//Jest on podlinkowany w Graphics.cpp, 2D.cpp i 3D.cpp

#if defined(G2D_H) and defined(G3D_H)
#include "2D.h"
#include "3D.h"
	class GraphicsBase : public Graphics2D, public Graphics3D
	{
	protected:
		GraphicsBase(FrameBuffer& rBuffer) :
			Graphics2D(rBuffer),
			Graphics3D(rBuffer)
		{};
	};
#elif defined(G2D_H)
#include "2D.h"
	class GraphicsBase : public Graphics2D
	{
	protected:
		GraphicsBase(FrameBuffer& rBuffer) :
			Graphics2D(rBuffer)
		{};
	};
#elif defined(G3D_H)
#include "3D.h"
	class GraphicsBase : public Graphics3D
	{
	protected:
		GraphicsBase(FrameBuffer& rBuffer) :
			Graphics3D(rBuffer)
		{};
	};
#else 
	class GraphicsBase
	{
	protected:
		GraphicsBase(FrameBuffer& rBuffer)
		{};
	};
#endif

class Graphics : public GraphicsBase //Klasa właściwa
	{
	private:
		FrameBuffer& Buffer;
	public:
		Graphics(FrameBuffer& rBuffer);
		void Fill(unsigned int uiColor);
	};

2D.h

#define G2D_H

struct FrameBuffer;

class Graphics2D
{
protected:
	FrameBuffer& Buffer;
public:
	Graphics2D(FrameBuffer& rBuffer);
	void Kwadrat();
}

2D.cpp

#include "2D.h"
#include "Display.h" //Tu jest definicja structa FrameBuffer

Graphics2D::Graphics2D(FrameBuffer& rBuffer) :
	Buffer(rBuffer)
{}

void Graphics2D::Kwadrat()
{}		

3D.h

#define G3D_H

struct FrameBuffer;

class Graphics3D
{
protected:
	FrameBuffer& Buffer;
public:
	Graphics3D(FrameBuffer& rBuffer);
	void Szescian();
}

3D.cpp

#include "3D.h"
#include "Display.h" //Tu jest definicja structa FrameBuffer

Graphics3D::Graphics2D(FrameBuffer& rBuffer) :
	Buffer(rBuffer)
{}

void Graphics3D::Szescian()
{}		

Core.h

#include "Graphics.h"

class Core //Klasa główna, agregator dla pozostałych
{
public:
   Graphics GTX;
}

Main.cpp

#include "2D.h"
#include "3D.h"
#include "Core.h"

Teoretycznie działa, bo mogę sobie wywołać np. GTX.Kwadrat() albo GTX.Szescian, wszystko się uruchamia i kompiluje, ale potem program się sypie. Nie odpowiada na komendy, nie działają klawisze, a po kilku sekundach się zawiesza. Co dziwne, jeżeli w Graphics.h podlinkuje 2D.h i 3D.h bezwarunkowo, to wszystko działa normalnie. Z warunkiem (po wykryciu definicji 2D_H i 3D_H) niestety nie.

Ktoś ma jakiś pomysł co robię źle?

0

A oddzielnie te 3 wersje się kompilują?

Pierwszy program main.c z tylko 2D include g++ kompilacja
Druga tylko z 3D
i 3 z Obiema.

Jeśli ten etap przechodzi można to rozdzielić dodając przy głównym pliku flagę, którą preprocesor odczyta i jeśli ten warunek #ifdef będzie zadeklarowany to się doda.

Najlepiej wrócić do początku i zweryfikować czy te podstawowe wersje działają jak powinny, bo potem będzie znacznie mniej jasne co poszło nie tak, a w ten sposób już pewien problem się rozwiąże czy te rozwiązanie działa, dla każdego z wyjątków.

0

Ja to bym w ogóle przemyślał konstrukcję takiego softu bo coś mi się widzi że takie kombinacje są nie potrzebne ale pomyśl np. o szablonach
https://stackoverflow.com/questions/16358804/c-is-conditional-inheritance-possible

0

Błąd musi być gdzieś w kodzie, próbowałeś jakieś breakpointy ustawiać żeby sprawdzić, w którym miejscu się wykłada program?

1

Brak strażników w plikach .h

#include "2D.h"
#include "3D.h"
#include "Core.h" => #include "Graphics.h" => #include "2D.h" and/or #include "3D.h"
0

Po tych skrawkach konstruktorów class wydaje się wszystko ok, FrameBuffer to specyficzna nazwa, robisz swój system?

Ja bym to zdebugował tak czy siak, to jest najłatwiejszy sposób namierzenia problemu.

Czasem problem może siedzieć gdzieś głębiej, jak klawiatura siada i inne rzeczy to to w samym prologu konstruktora nie doświadczymy.

0

Ogólnie jak system działa na jednym rdzeniu to musi powiadomić system, że odczyt z stdin np. ma być O_NONBLOCK wtedy asynchronicznie odczytuje bez blokowania aplikacji, chyba że jest kilka wątków.

Czyli jak działał na jednym wątku i coś się zwaliło to zawiesiło całą aplikację.

0

To jest na pewno jakiś błahy błąd, jak się znajdzie miejsce gdzie to się wykłada, to łatwo się zrozumie co było powodem błędu.
Debugowanie jest trudne i dlatego nikt tego nie lubi.

Ja mogę się założyć, o flaszkę, że to na bank będzie coś głupiego i prostego co wykładało cały program, ale dopiero namierzenie miejsca błędu umożliwiło zrozumienie dlaczego tak się dzieje.

1

Jak @revcorey wcześniej doradził pomyśl o użyciu w tym przypadku szablonów. Zobacz przykład wykorzystujący variadic templates w dziedziczeniu.

#include <iostream>

using namespace std;

struct DummyBuffer{};

class Graphics2D
{
protected:
    DummyBuffer& Buffer;
public:
    Graphics2D( DummyBuffer& rBuffer ) : Buffer{rBuffer} {}
    void Kwadrat(){ cout << "Kwadrat\n"; }
};

class Graphics3D
{
protected:
    DummyBuffer& Buffer;
public:
    Graphics3D(DummyBuffer& rBuffer) : Buffer{rBuffer} {}
    void Szescian(){ cout << "Szescian\n"; }
};

template< typename... Interfaces >
class GraphicsBase : public Interfaces...
{
  public:
    GraphicsBase(DummyBuffer& rBuffer) : Interfaces(rBuffer)... {}
};

int main()
{
    DummyBuffer someBuffer;

    GraphicsBase Base {someBuffer};
    GraphicsBase<Graphics2D> Base2D {someBuffer};
    GraphicsBase<Graphics3D> Base3D {someBuffer};
    GraphicsBase<Graphics2D,Graphics3D> BaseAll {someBuffer};

    Base2D.Kwadrat();
    Base3D.Szescian();
    BaseAll.Kwadrat();
    BaseAll.Szescian();
}

https://godbolt.org/z/8Mxnoh5Px

0

Może powinieneś użyć wzorca projektowego plugin?

0

Uprościłem przykładowy kod, żeby wywalić wszystko co zbędne. Nadal wychodzą mi dziwne efekty. Mam coś takiego (teraz to już 100% wklejki):

Base.h

#pragma once

//#define TRIGGER - BARDZO WAŻNA WSKAZÓWKA!!!

#if defined(TRIGGER)
#include "Extra.h"
#endif

namespace ce
{
#if defined(TRIGGER)
	class Base : public Extra
	{
	public:
		Base(int& iPassThing);
		void BaseCall();
	};
#else
	class Base
	{
	public:
		Base(int& iPassThing);
		void BaseCall();
	};
#endif
}

Base.cpp

#include "Base.h"
#include <iostream>

namespace ce
{
#if defined(TRIGGER)
	Base::Base(int& iPassThing) :
		Extra(iPassThing)
	{
	}
#else
	Base::Base(int& iPassThing)
	{
	}
#endif

	void Base::BaseCall()
	{
		std::cout << "Base Call" << std::endl;
	}
}

Extra.h

#pragma once

namespace ce
{
	class Extra
	{
	public:
		int& Thing;
		Extra(int& iThing);
		void ExtraCall();
	};
}

Extra.cpp

#include "Extra.h"
#include <iostream>

namespace ce
{
	Extra::Extra(int& iThing) :
		Thing(iThing)
	{
	}
	
	void Extra::ExtraCall()
	{
		std::cout << "Extra Call" << std::endl;
	}
}

Core.h

#pragma once
#include "Base.h"

namespace ce
{
class Core
	{
	public:
		int SomeGarbage = 100;
		Base B;
		Core();
	};
}

Core.cpp

#include "Core.h"

namespace ce
{
	Core::Core()  :
		B(SomeGarbage),
	{
	}
}

Main.cpp

#define TRIGGER
#include "Core.h"

Zwróćcie proszę uwagę na kolejność. Na samiutkim początku (w Main.cpp) pojawia się definicja TRIGGER. Po niej zostaje zalinkowane Core.h, które to linkuje Base.h. Czyli w momencie "wywołania" Base.h, definicja TRIGGER już istnieje, prawda? No tak się zdaje i rzeczywiście blok kodu zależny od istnienia tej definicji TRIGGER zostaje skompilowany ale występuje błąd, o którym pisałem wcześniej (nic nie odpowiada, a po kilku sekundach się sypie). Co ciekawe, wszystko jest OK, gdy definicja TRIGGER nie następuje na początku Main.cpp, a dopiero na początku Base.h (zobaczcie wykomentowany fragment tego nagłówka). To jest dziwne, bo przecież teoretycznie nie przestawia to nic w kolejności linkowania, nadal najpierw zostaje wywołane Core.h, które to wywołuje Base.h, które to zawiera definicję TRIGGER. Pomyślałem, że może z jakiegoś powodu Core.h musi być wywołane jako pierwsze, a definicja TRIGGER może się pojawić dopiero wtedy, gdy Core.h zostało już zalinkowane... ale nie. Gdyby obie definicje pojawiają się równocześnie (zarówno w Main.cpp jak i w Base.h) wszystko działa jak trzeba, mimo że przecież teoretycznie ta z Main.cpp nadal jest wywoływana jako pierwsza, jeszcze przed wywołaniem Core.h...?

Rozumie coś ktoś z tego?

EDIT

Dodatkowo problem znika po wywaleniu z klasy Extra pola Thing. Nawet po przerobieniu go na zwykłego inta (a nie referencję) i wywaleniu jego przypisywania w konstruktorze (zastępując je całkowitym brakiem inicjalizacji lub przypisując mu wartość początkową na płasko - int Thing = 100) nadal się sypie. Dopiero całkowite skasowanie pola usuwa problem. Innymi słowy, klasa Extra bez pól działa, z polami (jakimikolwiek, zainicjowanymi lub nie) nie działa. No chyba, że TRIGGER pojawia się w Base.h, wtedy pola nie przeszkadzają. Całkowite wywalenie konstruktorów z klas Base i Extra też nie pomaga, o ile ta druga ma pola.

0

Kolejne testy pokazują, że to nie ma nic wspólnego z linkowaniem, tylko z dyrektywami! Spójrzcie na ten kod:

Base.h

#pragma once
#include <iostream>

//#define TRIGGER

namespace ce
{
	class Extra
	{
	public:
		int JakiesPole = 100;
		void ExtraCall()
		{
			std::cout << "Extra Call" << std::endl;
		};
};
	
	class Base
#if defined(TRIGGER)
	: public Extra
#endif
	{
	public:
		void BaseCall()
		{
			std::cout << "Base Call" << std::endl;
		};
	};
}

Wystarczy "odkomentować" #define TRIGGER (#define TRIGGER w Main.cpp nadal istnieje) albo "wykomentować" JakiesPole i problem znika.

0

Takie rzeczy jak jakieś flagi to się przeważnie w jakiś makach robi a nie w kodzie CPP bezpośrednio. trzy po trzy śledzę temat. Co ty chcesz ostatecznie osiągnąć?

1

Jak już pisałem, modularną klasę. Np. bazowo masz funkcję z klasy Figury.Kwadrat() ale jak dodasz nagłówek #include Kolo.h, to w klasie Figury pojawi ci się Figury.Kolo()itd. Piszę rodzaj frameworka, który może robić wiele rzeczy, ale nie wszystkie są potrzebne w każdym projekcie, więc nie wszystko musi być zalinkowane do klasy fasadowej, a tylko to, co jest akurat przydatne.

Całkowicie niepotrzebna gmatwanina. Co więcej niezbyt to dla mnie intuicyjne. Ja chciałbym mieć stabilne API klasy tak jak masz np. w qt i sprawdzić w dokumentacji co i jak. A nagle w projekcie mają mi się pojawiać klasy o tej samej nazwie z różnym interfejsem bo ktoś dodał kolo.h. A jak trzeba będzie jednocześnie użyć kolo.h i trojkat.h w tym samym pliku to co się ma stać? A jak wielu ludzi będzie pracować nad kodem to co? Widzę egzemplarz klasy X i się okazuje że ma on inny interfejs bo ktoś przede mną dodał kolo.h.

Klasa teoretycznie powinna robić jedną rzecz albo obsługiwać jakiś mechanizm znowu sięgnę do qt. masz tam abtractmodel a po nim dziedziczą inne bardziej szczegółowe modele. Przyjrzyj się założeniom Qt.

Moje osobiste zdanie jest takie że źle zabierasz się do tematu. I nie wiem co chcesz zyskać takim podejściem.

A co do dyrektyw powtórzę dodaj je w make.

0

Hmm, odpal w ida pro/ghidra czy innym disassemblerze i porównaj działającą aplikację i tą wygenerowaną z dyrektywami preprocesora.

Mi taka analiza często pomagała gdy np. linkier w złym miejscu kod położył czy inne problemy, sprawdziłem adresy relatywne itp.

Tak chyba będzie najszybciej.

1
Crow napisał(a):

Wystarczy "odkomentować" #define TRIGGER (#define TRIGGER w Main.cpp nadal istnieje) albo "wykomentować" JakiesPole i problem znika.

Nie wiem czy dobrze zrozumiałem, ale czy oczekujesz że wystarczy #define TRIGGER w main.cpp, żeby wszędzie indziej miało efekt? TRIGGER trzeba zdefiniować w każdej jednostce kompilacji, czyli wszystkie pliki cpp muszą albo mieć albo nie mieć to zdefiniowane. Takie rzeczy definiuje się na poziomie całego projektu, np. w pliku CMakeLists.txt i przekazuje tą definicję do targetu.

0

Zrobiłem coś takiego:

Main.cpp

#define CHECK
#include "Core.h"

Core.h

#pragma once

#ifdef CHECK
#define TRIGGER
#endif

class Extra
	{
	public:
		int JakiesPole = 100;
		void ExtraCall()
		{
			std::cout << "Extra Call" << std::endl;
		};
	};

	class Base
#ifdef TRIGGER
	: public Extra
#endif
	{
	public:
		void BaseCall()
		{
			std::cout << "Base Call" << std::endl;
		};
	};

I robią się błędy, chociaż w Core.h "podświetla" się ta parta kodu, która powinna zostać skompilowana w razie wykrycia definicji TRIGGER (chyli kompilator uznaje, że warunek został spełniony - CHECK wykryto). Gdy natomiast wywalam sprawdzenie CHECK z Main.cpp i zostawiam bezwarunkowe #define TRIGGER w Core.h, wtedy działa bez zarzutu. Czyli kompilator niby poprawnie reaguje na obecność CHECK w Main.cpp, bo wie które partie kodu ma wykorzystać, ale jednak kompiluje z błędami.

0

Może jedno z trzech:

1:

#include <iostream>
#include <memory>
using namespace std;

//#define TRIGGER

class BaseBase
{
	public:
	virtual void BaseCall() { cout<<"Not avaliable (moze wyjatek?)"<<endl; }
	virtual void ExtraCall() { cout<<"Not avaliable (moze wyjatek?)"<<endl; }
	virtual ~BaseBase() {}
};

class BaseInterface:public BaseBase
{
	public:
    virtual void BaseCall() { cout<<"Base Call"<<endl; }
};

class ExtraInterface:public BaseInterface
{
	public:
	virtual void ExtraCall() { cout<<"Extra Call"<<endl; }
};

#ifdef TRIGGER
typedef ExtraInterface UsedInterface;
#else
typedef BaseInterface UsedInterface;
#endif

class UsedClass:public UsedInterface
{
};

int main()
{
	UsedClass bc;
	bc.BaseCall();
	bc.ExtraCall();
}

2:

#include <iostream>
#include <memory>
using namespace std;

//#define TRIGGER

class BaseBase
{
	public:
	void BaseCall() { cout<<"Base Call"<<endl; }
	void ExtraCall() { cout<<"Extra Call"<<endl; }
	void Unavailable() { cout<<"Not avaliable (moze wyjatek?)"<<endl; }	
};

class BaseFasade
{
	private:
	BaseBase bb;	
	public:
    void BaseCall() { bb.BaseCall(); }
    void ExtraCall() { bb.Unavailable(); }
};

class ExtraFasade
{
	private:
	BaseBase bb;	
	public:
    void BaseCall() { bb.BaseCall(); }
    void ExtraCall() { bb.ExtraCall(); }
};

#ifdef TRIGGER
typedef ExtraFasade UsedFasade;
#else
typedef BaseFasade UsedFasade;
#endif

class UsedClass:public UsedFasade
{
};

int main()
{
	UsedClass bc;
	bc.BaseCall();
	bc.ExtraCall();
}

3:

#include <iostream>
#include <memory>
using namespace std;

//#define ADD
//#define BASE
#define EXTRA

class BaseBase
{
	public:
	void Unavailable() { cout<<"Not avaliable (moze wyjatek?)"<<endl; }	
	void AddCall() { Unavailable(); }
	void BaseCall() { Unavailable(); }
	void ExtraCall() { Unavailable(); }
	virtual ~BaseBase() {}
};

class UsedClass:public BaseBase
{
	public:
#ifdef ADD
	void AddCall() { cout<<"Add Call"<<endl; }
#endif
#ifdef BASE
	void BaseCall() { cout<<"Base Call"<<endl; }
#endif
#ifdef EXTRA
	void ExtraCall() { cout<<"Extra Call"<<endl; }
#endif
};

int main()
{
	UsedClass uc;
	uc.AddCall();
	uc.BaseCall();
	uc.ExtraCall();
}
3

Mam wrażenie, że próbujesz wymyślić na nowo
https://en.m.wikipedia.org/wiki/Curiously_recurring_template_pattern

Dzięki temu obejdzie się bez żadnego użycia preprocesora, jednocześnie nie pozbawiając siebie statycznych checków od kompilatora, czy metoda powinna istnieć czy nie (tak jak w pierwszym przykładzie Dragona).

5

Co ciekawe, wszystko jest OK, gdy definicja TRIGGER nie następuje na początku Main.cpp, a dopiero na początku Base.h

Jak TRIGGER jest w main.cpp, to plik base.cpp nie widzi TRIGGER. Dla pliku main.cpp klasa Base dziedziczy po Extra. Dla base.cpp nie dziedziczy. Masz 2 definicje tej samej klasy, które mają różny układ w pamięci i niezainicjowaną klasę bazową.

Dodatkowo problem znika po wywaleniu z klasy Extra pola Thing.

Bo po wywaleniu pola thing klasa Extra nie wpływa na rozmiar klasy dziedziczącej - nie ma żadnych pól - więc znika problem 2 różnych układów w pamięci dla obiektów tego samego typu.

Mam nadzieję, że to wystarczy by przekonać Cię, dlaczego ten pomysł jest chybiony.

Dodatkowo, mylisz linkowanie z "inkludowaniem" (dodawaniem nagłówków) albo nie rozumiesz jak przebiega proces wytworzenia pliku wykonywalnego. Nagłówki dodaje preprocesor i w praktyce sprowadza się to do zwykłego wklejenia zawartości nagłówków do innego pliku. Jest to pierwszy etap w procesie. Linkowanie - czy też po polsku konsolidacja - to łączenie ze sobą różnych plików z już skompilowanym kodem w nowy plik ze skompilowanym kodem. Za ten etap odpowiada linker i jest to ostatni etap w procesie. Kolejność dołączonych nagłówków nie ma wpływu na kolejność linkowania plików.

0

Ustaliliśmy, że mój pomysł nie był dobry, jakiej zatem powinienem użyć alternatywy, by osiągnąć mniej więcej podobny efekt (modularność)? Dziedziczenie przez template i parameter pack? A może lepiej zastąpić parameter pack to przy pomocy initializer_list? W przypadku klas upakowanych w parameter packu da się w ogóle zrobić tak, żeby każda z klas miała inny konstruktor, przyjmujący inne argumenty? Czy też wszystkie muszą mieć identyczny konstruktor?

0

Ja dalej nie rozumiem jaki to ma być efekt i jaki problem rozwiązuje. Bo modularność, to trochę coś innego niż opisujesz. C++20 ma wsparcie dla modułów, ale nie działają tak jakbyś oczekiwał. Modularność to sposób podziału kodu na mniejsze jednostki, zazwyczaj pod względem dostarczonej funkcjonalności. Polimorfizm - czy to statyczny czy dynamiczny - to nie modularność.

2

Generalnie chcę osiągnąć efekt, w którym zawartość klasy fasadowej Core, będzie zależna od wyboru użytkownika. Czyli jak chce mieć dostęp np. do funkcji 3D, to podaje klasę 3D jako parametr w konstruktorze fasady i wtedy te funkcje będą tam widoczne. Nawet niekoniecznie w Core.Graphics, tylko np. w Core.Graphics3D, czyli jako osobny interface. I nie chodzi tu o żaden problem, tylko sposób organizacji kodu i pracy z frameworkiem.

Generalnie klasa powinna mieć zdefiniowaną funkcjonalność, a nie morfować według widzimisię użytkowników-developerów. Idealnie uważną się, że w programowaniu obiektowym klasa powinna odpowiadać za pojedyncza funkcjonalność. Zobacz zasadę SOLID SRP

Można sterować tym co robi jakiś kawałek kodu poprzez preprocesor. Nie jest to piękne, ani łatwe w czytaniu, ale się tego używa. Tylko zdefiniowanie co robi klasa powinno być spójne wewnątrz projektu, a nie tak jak zrobiłeś to Ty, tzn definy powinny być ustalone raz podczas czasu kompilacji. Użytkownik przekompiluje sobie Twój kod ze swoimi zmiennymi preprocesora, właczając to co potrzebuje. Z tego co pamiętam, zostało to już zasugerowane na początku dyskusji przez kogoś. Raczej stosuje się to w programowaniu niskopoziomowy do obsługi różnych architektur lub konfigurowalnych podczas kompilacji wersji produktu.

Ale funkcjonalność klasy powinna być niezmienna, spójrz na kolejną zasadę z serii SOLID - open-close. Jakieś magiczne morfowanie tego co klasa wystawia w zależności od czegoś innego ... to nie zadziała w cpp zbyt dobrze. Potrzebujesz osobnych klas.

Można natomiast zmieniać zachowanie konkretnych instancji klasy. Wystarczy, że opcje zostaną przekazane do nowo tworzonej instancji klasy fasadowej w konstruktorze. Kod musiałby sprawdzać stan opcji, by wiedzieć, które kawałki kodu uruchomić. W Twoim przypadku nie jest to znowu zbyt piękne. Nie ukrywa też zbędnych metod z klasy, bo te po prostu do niej przynależą, mogą co najwyżej rzucać wyjątkiem lub nic nie robić, co jest bardzo słabe. Ale też się czasami stosuje, choć raczej w inny sposób, sterując na przykład protokołem komunikacji.

Innym podejściem, który rozwiązuje zbliżony problem, są mixiny, czyli klasy, które dostarczają konkretną funkcjonalność, gotową do wykorzystania w klasach dziedziczących. Odpowiednie zastosowanie wieledziedziczenia. Zamiast dostarczać użytkownikowi jakąś klasę fasadową możesz mu dostarczyć klasy dostarczające funkcjonalność i wymagać od niego by dziedziczył po klasach, których funkcjonalności potrzebuje. I to może być jak najbardziej zastosowanie dla CRTP, aczkolwiek można też pominąć szablony w implementacji.

Możesz też dostarczyć różne kombinacje klas core zapewniających odpowiednią funkcjonalność. Pod spodem wykorzystać na przykład kompozycję albo mixiny. Wtedy rzeczywiście osiągnąłbyś efekt widoczności odpowiednich metod. Tylko żeby tych kombinacji nie było zbyt dużo, bo to jest znowu bez sensu.

Na koniec - zapoznaj się z zasadami SOLID i sprawdź, czy nie próbujesz umyślnie złamać którejś z nich.

0

Zamiast dostarczać użytkownikowi jakąś klasę fasadową możesz mu dostarczyć klasy dostarczające funkcjonalność i wymagać od niego by dziedziczył po klasach, których funkcjonalności potrzebuje.`

Chciałbym mieć jedno i drugie. Fasadową klasę bazową Core, która dostarcza kilku metod abstrakcyjnych niezbędnych do działania całości (jest to prosty game engine z możliwością renderowania 2D i 3D), np. OnUserUpdate() albo OnUserInput(), natomiast cała reszta powinna zależeć od wyboru użytkownika, np. właśnie w formie dziedziczenia po tych klasach. Czyli użytkownik MUSI dziedziczyć po Core, gdzie ma zebrane esencjonalne dla całości elementy, natomiast inne składniki (klasy) to już wedle potrzeby. Chciałbym ro mieć w konstruktorze dla Core, np. w formie variadic template. To jest OK podejście?

1
Crow napisał(a):

Zamiast dostarczać użytkownikowi jakąś klasę fasadową możesz mu dostarczyć klasy dostarczające funkcjonalność i wymagać od niego by dziedziczył po klasach, których funkcjonalności potrzebuje.`

Chciałbym mieć jedno i drugie. Fasadową klasę bazową Core, która dostarcza kilku metod abstrakcyjnych niezbędnych do działania całości (jest to prosty game engine z możliwością renderowania 2D i 3D), np. OnUserUpdate() albo OnUserInput(),

Na czym polega fasadowość tej klasy? Dla mnie brzmi jak interfejs, który musi zaimplementować użytkownik, czyli w CPP abstrakcyjna klasa bazowa. Bo kiedy mówisz o klasie fasadowej, to ja myślę o wzorcu fasada

natomiast cała reszta powinna zależeć od wyboru użytkownika, np. właśnie w formie dziedziczenia po tych klasach. Czyli użytkownik MUSI dziedziczyć po Core, gdzie ma zebrane esencjonalne dla całości elementy, natomiast inne składniki (klasy) to już wedle potrzeby. Chciałbym ro mieć w konstruktorze dla Core, np. w formie variadic template. To jest OK podejście?

To ustalmy jeszcze jedno - co się stanie jak użytkownik dokona jakiegoś wyboru? Jaki to ma wpływ na funkcjonalność?

0
nalik napisał(a):

To ustalmy jeszcze jedno - co się stanie jak użytkownik dokona jakiegoś wyboru? Jaki to ma wpływ na funkcjonalność?

Ja bym to widział tak. Użytkownik dziedziczy tylko po Core, więc dostaje tylko niezbędne funkcje, czyli np. Core.Graphics.ClearScreen() albo Core.Graphics.DrawSquare(). Ale jak zrobi dziedziczenie po np. Sound, to dostanie dostęp do Core.Sound.PlaySound(), a jak po Keyboard, to zyska dostęp do Core.Keyboard.GetKeyState() itd. To jest poprawne?

1
Crow napisał(a):
nalik napisał(a):

To ustalmy jeszcze jedno - co się stanie jak użytkownik dokona jakiegoś wyboru? Jaki to ma wpływ na funkcjonalność?

Ja bym to widział tak. Użytkownik dziedziczy tylko po Core, więc dostaje tylko niezbędne funkcje, czyli np. Core.Graphics.ClearScreen() albo Core.Graphics.DrawSquare(). Ale jak zrobi dziedziczenie po np. Sound, to dostanie dostęp do Core.Sound.PlaySound(), a jak po Keyboard, to zyska dostęp do Core.Keyboard.GetKeyState() itd. To jest poprawne?

Tak, to powinno być w innej klasie. Ale niekoniecznie powinieneś zmuszać użytkownika do dziedziczenia po obu klasach. To brzmi jak zastosowanie dla kompozycji.

Dziedzicznie miałoby sens, jakby to użytkownik definiował zachowanie, które ma uruchomić framework. Jeżeli to framework dostarcza konkretną funkcjonalność, to nie ma konieczności dziedziczenia po klasie zapewniającej ową funkcjonalność, aby tą funkcjonalność uruchomić, a wręcz jest to błąd projektowy.

Przykładowe, jeżeli developer-użytkownik-frameworku ma mieć możliwość odtworzenia dzwięku, to może stworzyć sobie instancję klasy Core.Sound i uruchomić odpowiednie metody. Po co tutaj dziedziczenie?

PS. zakup i przeczytaj https://helion.pl/ksiazki/czysty-kod-podrecznik-dobrego-programisty-robert-c-martin,czykov.htm#format/d

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