Hello, I would like to know how you guys deal with circular dependencies. Currently my flow is there is a Module script which is a Unit Manager which created/removes/stores Units so it would require the Unit class. Now the Unit class has it’s own StateMachine and States but in the States module script there are states which may need access to all the units in the game (example, to find the nearest unit to attack). I would end up with a circular depency like UnitManager → Unit → StateMachine → State → UnitManager. How do you guys usually deal with this problem?
Do you mean to pass the whole units list into the StateMachine tick() as data? I was thinking about going with @halosviel 's suggestion of a middle man module like UnitStorage but I am not sure what the usual convention is.
Also in states they contain transitions which are they ones that set the target as they do the searching.
Well the idiomatic way of breaking up cyclic dependencies is indeed having a shared module with no dependencies, but in your case I’m not certain whether if it would work, as you implied that units have a hard dependency on states. Therefore I suggested you use dependency injection instead.
There isn’t much else I can comment on regarding this since I don’t have access to your codebase.
You don’t need to do that. Just pass UnitManager to the StateMachine’s blackboard once and then query units using that reference.
Usually circular dependencies are the direct result of badly designed systems. You should create a visual of all the relationships between your modules, look at the cycles formed, and then figure out what shape that doesn’t include a cycle makes sense the most. Sometimes for example it can be a missing class you haven’t implemented or figured out yet. It can also be that a class is in the wrong location (relative to usage it should have another parent or descendant).
Alternatively you can completely bypass circular dependency issues by loading everything inside a single loader, but that kind of defeats the whole purpose of following proper paradigms.
Yes, you would do exactly that. To understand how you pass down UnitManager, refer to the following graph:
UnitManager → Unit → StateMachine → State --X-> UnitManager
^^^^^
Break the cycle here
Now since UnitManager already links up to State via the call stack, you can simply pass it down through it like:
-- in UnitManager
Unit.new(unitManager)
-- in Unit
StateMachine.new(self.unitManager)
-- in StateMachine
State.new(self.unitManager)
This way you can keep intellisense and everything, all you have to do is have a shared UnitManager type.
Though at this point I think @NyrionDev is right, your dependency graph is generally strange- I think it might be better to just start over because hard cyclic dependencies like this are usually a symptom of bad system design.
Would I be right to say that I should have made a Type module, declaring the values and methods of UnitManager where both UnitManager and States can require() it. So in the end it would be like:
UnitManager → Unit → StateMachine → State -> UnitManagerType
Like a header file in C++/C?
So now I would have type annotation and intellisense when doing:
local UnitManager: UnitManagerType.UnitManager = data.UnitManager
local units: {UnitType.Unit} = data.UnitManager.GetAllUnits()
inside the State (in my case it’s Transition which is what State contains)
Totally agree with this, other than the dependency injection you suggested, I am still unsure on what I can do differently to fit with the general convention. Would the explanation above be a proper way to do it or is there something seriously wrong with my design? I am okay with redesigning but I am not sure how and where to start.
Yes. Even this could be entirely avoided if Luau had weak imports (type-only), but importing is 100% runtime so it’s unfortunately not possible.
It’s kind of up to you to decide, there isn’t anything inherently bad about your design, dependency-injection here should be enough. That’s how I would solve it, but from the start, that’s why I suggested you remake everything. There might even be other cyclic dependencies that you don’t see right now.
Just an update, I have decided to not used type annotations in my state and transitions so now instead of: function module:OnUpdate(unit: Unit.Unit)
I am doing: function module:OnUpdate(data)
In my chasing state. I felt that having another Type module just adds to the complexity where I have to update both scripts when adding a function and I rather just code without intellisense for example, doing:
data.Instance:GetPivot()
I would just rely on knowing what I passed into the OnUpdate function