-
Notifications
You must be signed in to change notification settings - Fork 4
Dependency Management
Inversion of Control (IoC) is a software design principle that facilitates the creation of more flexible and easily manageable systems. In traditional programming approaches, control over the flow of execution and object creation is handled directly in the code. In contrast, IoC assumes that control is passed to an external component or container that is responsible for creating and linking objects.
One popular way to implement IoC is to use Dependency Injection (DI). In this case, class dependencies are passed to them externally, which reduces the connectedness of components and simplifies their testing. The use of IoC improves code readability, its reusability and facilitates the process of making changes, which is especially important in the context of modern software development requirements.
The engine already has a built-in implementation in the form of a DependenciesContainer
, as well as a DependencyManager
to make things easier.
DependencyManager is the main way to enforce dependencies, it is a root container from which all inherit and which provides the basic dependencies.
Since DependencyManager
provides a safe wrapper over the DependenciesContainer
. Most of its methods are identical to those of the container itself.
Note
This class is safe for use in multithreaded code. Each thread has its own DependenciesContainer
, and if you register a new dependency in a thread, it will be available only there and nowhere else. Also, when creating a thread, you must reinitialize the manager there through the InitThread
method.
In order to register a dependency, you will need the Register
method and the various ways to do this will be described below.
The easiest way is to pass the type interface, as well as its implementations. But this requires that the implementation has no constructors accepting ANY parameters, or no constructor at all.
DependencyManager.Register<IService, Service>();
You can also dispense with a separate type and specify the implementation directly.
DependencyManager.Register<Service>();
// You can also write it that way, it's an equivalent expression.
// DependencyManager.Register<Service, Service>();
If you need to create your dependency in a special way, you can override the factory method that takes the collection itself as the first argument, for convenience.
DependencyManager.Register<IService>(collection => new Service());
You can also create a registerable type outside the manager, which is not recommended, and it is better to use the factory overload described above.
var instance = new Service();
DependencyManager.Register<IService>();
Resolve dependencies is done via the Resolve
method and also requires its registration, any getting of an unregistered dependency will throw an exception.
var service = DependencyManager.Resolve<IService>();
Lazy dependencies are those that are created only when someone asks for them.
Note
All dependencies are lazy by default, and if you don't use them anywhere, they won't get created, which may break your logic. To solve this problem, use the Instatiate
methods described below.
Note
It should be noted that if a dependency has already been created, these methods cannot create it again.
In order to force dependency creation, you need to call the Instantiate<T>
method and pass the dependency type.
DependencyManager.Instantiate<IService>();
You may also need to create all dependencies in the container, to do this use InstantiateAll
DependencyManager.InstantiateAll();
Of course, we couldn't help but touch on this important topic that provides us with convenience when dealing with dependencies. Namely, dependency injection itself.
Note
In this case, default!
means that the value will be equal to null
before injection, but will not mark the type as nullable. So if you try to interact with the dependency before injection, you will get a NullReferenceException
.
In order to declare a field in a class as a dependency, we have DependencyAttribute.
public sealed class Service
{
[Dependency] private readonly IInputHandler _inputHandler = default!;
}
In order to implement already declared dependencies, you need to call the Inject
method and pass the object in which you want to implement them, and often it will be the same class.
public sealed class Service
{
[Dependency] private readonly IInputHandler _inputHandler = default!;
public Service() {
DependencyManager.Inject(this);
}
}
It is also important to note that the dependencies to be implemented are not available inside the constructor, and if you need to process the dependency after implementation, you need to implement the IPostInject
interface, whose method will be called after all dependencies have been implemented.
public sealed class Service : IPostInject
{
[Dependency] private readonly IInputHandler _inputHandler = default!;
public Service()
{
DependencyManager.Inject(this);
// Throws NullReferenceException becouse _inputHandler is null now
// _inputHandler.IsKeyDown(Key.Escape);
}
public void PostInject()
{
if (_inputHandler.IsKeyDown(Key.Escape)) {
// Do some code...
}
}
}
I would also like to note that dependencies inside the dependencies are implemented automatically, you don't need to call Inject
.
// In this example, there is no registration code, but there should be.
public sealed class Foo
{
[Dependency] private readonly IService _service = default!;
public Foo()
{
DependencyManager.Inject(this);
}
}
public sealed class Service : IService, IPostInject
{
// Inject automaticly
[Dependency] private readonly IInputHandler _inputHandler = default!;
public void PostInject()
{
if (_inputHandler.IsKeyDown(Key.Escape)) {
// Do some code...
}
}
}
If you build the game and the engine in debug mode, you will get an additional logging warning under certain conditions. This is not done in other builds for optimization reasons.
Warning
Attempting injecting in <target> already exists <dependency type> to <field name>
You will receive this warning if your dependency already exists when injecting. This happens most often when double injecting, or injecting into a dependency.
Warning
Attempting injecting in <target> without valid dependencies
You will get this warning if you try to inject into an object that does not have any fields marked as dependencies. This is most often caused by removing dependencies, but not injecting.
Since the engine itself is written with active use of dependencies, you can easily change any of its services to your own by rewriting the entry point.
public static class Program
{
public static void Main(string[] args)
{
var parser = new ArgumentParser(args);
parser.TryParse();
var rootContainer = DependencyManager.InitThread();
// Just copy code of all Engine dependencies from Dependencies.cs (Server or Client)
SharedDependencies.Register(rootContainer);
// Input
rootContainer.Register<IInputHandler, InputHandler>();
rootContainer.Register<IInputManager, InputManager>();
// Audio
rootContainer.Register<IAudioLoader, AudioLoader>();
rootContainer.Register<IAudioManager, OpenAlAudioManager>();
// Texturing
rootContainer.Register<ITextureManager, TextureManager>();
// Camera
rootContainer.Register<ICameraManager, CameraManager>();
// Rendering
rootContainer.Register<IRenderer, CustomRenderer>(); // For example change renderer
rootContainer.Register<IImGui, Graphics.ImGui.ImGui>();
// Runtime
rootContainer.Register<IRuntimeLoop, RuntimeLoop>();
rootContainer.Register<IRuntime, Runtime>();
rootContainer.InstantiateAll();
var runtime = rootContainer.Resolve<IRuntime>();
runtime.Run();
}
}