Currently, much of the plugin code relies on the Service Locator pattern to fetch dependencies, which seems to be an anti-pattern as it makes code harder to test.
Before refactoring, we would have to consider how to perform dependency injection in the first place.
Coming from Unity, I found the best way to achieve composition within prefabs is through a base class script on the main prefab object (the prefab object that you can edit when dragging the prefab into the scene). The base class exposes events that allow for other classes to hook into the behavior of the base class.
For example, consider creating prefabs that may have different variants (such as an enemy prefab having different enemy types). The base class here would be Enemy
and would contain only events and variables that are present in all enemy types. This could include things like a health_controller
since all enemies can be damaged. These variables are then manually assigned by the developer using the inspector.
If a prefab variant ever needed more external dependencies, I could add another script to the main prefab object that requested for the external dependencies, and then I could manually assign them.
However, due to Godot using a 100% node-based system, I cannot add multiple scripts to a single node. One way of exposing dependencies is to replace the base class script of the prefab variants with a subclass script that extends from the base class script. However, I want to avoid using inheritance, as features like scene inheritance stop working due to replacing the base class script with a new script (which ultimately leads to more boilerplate work done to assign the base class script's dependencies for every new prefab variant made).
My plan so far is to use this format for each prefab scene,
PrefabRootNode <- [main_prefab_script.gd]
- Dependencies
- Dependency1 <- [prefab_dependency1.gd]
- Dependency2 <- [prefab_dependency2.gd]
main_prefab_script.gd
would have a reference to the Dependencies
node by incorporating this snippet of code:
export var dependencies_node_path: NodePath
onready var dependencies = get_node(dependencies_node_path)
Then all scripts that need to set the external dependencies of the prefab would have to look into the children of Dependencies
.
This allows for easier extension of the prefab as new external dependency requests can simply be added as another node under Dependencies
.
However, to actually adjust the external dependencies of the prefabs, you have to enable editable children
on the prefab node, which makes it slightly inconvenient to use as you must check editable children
every time you add the prefab to the scene. But adding dependencies from code would not be any different due to having a reference to Dependencies
in the base class script. that can be accessed with base_class.dependencies
.
But the biggest drawback to implementing dependency injection is the loss of quick customizability. For example, every time a developer wants to switch out a GUI they would have to manually reassign the dependencies. I think this works against the quick drag and drop nature of customizing the GUI.
I could try to automate this by having a custom dependency resolver but that would mean for each variant of the GUI that implements or removes certain external dependencies I would have to create a custom dependency resolver for them. This feels really unwieldy as now the developer would have to swap out the appropriate dependency resolver if they happen to add a new GUI that the current dependency resolver cannot resolve.
Then again, dependency injection is explicit in the dependencies needed by the prefab. With Service Location, a developer would only find out about missing dependencies after they hit a runtime error complaining about it.
I think in the end, manually assigning external dependencies for different variants of a prefab is inevitable. At most I can automate the assigning of the base class's dependencies but every other external dependency must be assigned by hand when the prefab is added to a scene. But I think the tradeoff of losing quick-and-drag and gaining explicit external dependency requests is worth it in the end, since developers would know exactly what external dependencies are needed by every prefab variant.