If you use inheritance you can skip circular dependancy issues.
The CardManager knows about the AbstractCard object, and then an object called PlayingCard inherits from AbstractCard and includes a loose reference to a Cardmanager object.
So now you have a Cardmanager that references AbstractCard objects and PlayingCard objects that can call methods from CardManager.
Circular references are actually useful sometimes but in modern languages when everything is a reference there is no distinction between objects and references, therefore ‘loosely coupled’ is harder to implement.
I dont know why there is a big problem about objects knowing about each other, except for possible memory issues with objects references hanging about when they are removed, causing design patterns where the deleted object calls functions to cause objects that have a reference to set the reference to the caller to null. (i.e. ref.null_my_reference() …)
I would use signals and use inheritance lightly in gdscript for performance reasons - but i would definitely use them to implement observers.
The signals are often described as basically an observer pattern, but theyre also similar to the publisher subscriber model especially if you use the signal bus. (See wikipedia on the Observer pattern, also note the C++ example contains a circular dependancy).
Cycle 1.
CardUI has functions that get called on card state changes … so perhaps CardUI is connected to a signal _on_state_changed(state) thats emitted by either the state machine or the states, and responsible for calling the appropriate member function.
When do you set any of this to null? To remove or destroy an object?
Cycle 2.
Option 1.Use a global variable and cut the spaghetti. The CharacterStats publishes the data to a globally available data source that is trivially accessed by the cards.
Option 2. Use signals to inform the character stats that information is required, pass a reference to the card and set the information.
How would this eliminate a circular reference? It just moves it to the base class while piling on more abstraction and more inheritance, increasing the overall entropy. You can equally well inject a loose reference into the original object.
Architectural problems are best solved by simplification of the system structure, not by doing the opposite.
The system structure might be more complicated than what im pointing out.
its just an example from C++ that should ensure that the precompilation does not contain a circular reference.
File 1: BaseCard.h
class BaseCard {
public:
// stuff that the manager can interact with ...
};
File 2: CardManager.h
#include "BaseCard.h"
class CardManager{
public:
ArrayList<BaseCard> cards;
};
File 3: PlayingCard.h
#include "CardManager.h"
class PlayingCard : public BaseCard{
public:
CardManager **lpCardManager;
};
I suppose you could say that in gdscript BaseCard is not needed because its just essentially a node and then store nodes in the cardmanager with an array. Then you could simply check if they have certain methods. But that is a circular dependancy if the cards know whats managing them.
Theres just simply loads of examples where objects know each other that are very useful, especially in data structures. Its like a data structure people would try to make in C++ but the preprocessor or the compiler would keep complaining.
So clearly the problems can occur when some process tries to crawl the scene tree and potentially gets stuck in a loop, or that the references dont get made null and theres extra data hanging about.
Obviously i would agree that if a global variable is performant, safe in terms of memory and threading, and cuts out the complexity then it can be the best choice.
My main argument is that in an ideally designed system there should never appear a need to “fix” circulars. This is of course not always possible in the real world so we have two typical bandaids: weak/injected refs and globalization. I don’t have anything against them but if you’re pulling them in a desperate attempt to “save” your system after realizing you painted yourself into a corner - then something is fundamentally wrong with the system design. In the long run it’ll likely need a ground up redoing anyway.
In the particular case of cards vs card manager, I already described how it should be re-done without circular references. Either that or just quick and dirty globalize/inject… and pray that it works out.
There is a fundamental flaw in OP’s approach in that they allow cards, which are at the bottom of the responsibility hierarchy, to command objects that are above them in the hierarchy. This flaw is repeated with other low level objects as well. And if they continue to build this system using this principle, things will just get worse and worse.
Btw, introduction of an additional base class is not needed even in C++. The classical way to handle this is with a forward declaration and moving #include to the end of the header file.
You might be right. I just did a google search and the problem of circular references is often stated as a chicken and egg issue with creation, and a possible memory problem with destruction.
The card UI/card state pattern seems a bit like a composite design pattern. The card manager in the design i described isnt a composite because it does not inherit from BaseCard.
I would need to know more about the relation type between each object in the CardUI/CardState system i.e. one to many or one to one etc. I had to check the name because it sounds like ‘composition’.
Anyway … the composite design pattern clearly creates these circular references. I think theres less of a problem in low level languages with pointers, references and actual objects. In interpreted languages where everything is a reference the pattern isnt so great.