Components
This page contains information on all the components that make up the Dependency Container and how they interact.
Overview
The following diagram denotes the top-level relationship between different components of the IdleKit Dependency Container system.
IContext
The IContext hosts the IContainer and IInstaller. It usually lives as a MonoBehaviour
component on a root GameObject containing all the child GameObjects in an Unity scene. All scripts under the root GameObject would be able to utilize the IContainer and have their dependencies injected. Please refer to MonoContext.cs
for a concrete implementation of IContext.
IInstaller
The IInstaller is the location where the static bindings are established and registered to the IBinder. In IdleKit, it is where the registrations of all the service level bindings take place such as the IServices and IActions.
Registering (binding) the IServices and IActions
protected virtual void BindServices()
{
...
_container.Bind<ICurrencyService>().To<CurrencyService>().AsSingleton().Conclude();
_container.Bind<IEconomyService>().To<EconomyService>().AsSingleton().Conclude();
_container.Bind<IEntityLoaderService>().To<EntityLoaderService>().AsSingleton().Conclude();
...
}
protected virtual void BindActions()
{
...
_container.Bind<AdvanceStageStateAction>().AsSingleton().Conclude();
_container.Bind<AscensionCompleteStateAction>().AsSingleton().Conclude();
_container.Bind<AutomateGeneratorStateAction>().AsSingleton().Conclude();
...
}
Note
You can override the IInstaller to provide different implementations of services or actions, as shown below.
protected override void BindServices()
{
...
base.BindServices();
//Replace the implementation for the contract type ICurrencyService.
_container.Unbind<ICurrencyService>();
_container.Bind<ICurrencyService>().To<SpecialCurrencyService>().AsSingleton().Conclude();
}
Implementations of IInstaller
These are the existing implementations of IInstaller in IdleKit:
- MonoInstaller - An abstract MonoBehaviour based implementation of IInstaller
- IKInstaller - The main installer in IdleKit that includes all static dependencies required. This class can be extended and overriden to specify additional non-MonoBehaviour based IBindings.
- IKMonoInstaller - Contains IKInstaller as a component and implements MonoInstaller so it can be attached to a GameObject
- StandardInstaller - Overrides IKInstaller for a TPB style idle game
- StandardMonoInstaller - Similar to the IKMonoInstaller but uses StandardInstaller as its component
- SimpleInstaller - Extends StandardMonoInstaller and overrides functionality to bind PlotPoint related dependencies
IContainer
The IContainer contains the main logic of the container. It is made up of the IResolver and IBinder interfaces. No class other then the IContext should ever cache a reference to an IContainer. Instead, they should cache the IResolver or IBinder for the sake of interface segregation. Please refer to Container.cs
for a concrete implementation of IContainer.
IBinder
The IBinder is used to register bindings that describe the relationship between the classes. It allows bindings to be created, registered, or removed from the container. Once an IBinding is bound, a record of the IBinding is stored in the IBinder.
Warning
Conclude() must be called at the end of each Bind statement.
Using IBinder, usually in an IInstaller
//Register binding with contract type Foo to concrete type Bar with a null id.
_binder.Bind<Foo>().To<Bar>().Conclude();
//Register binding with contract type Foo to concrete type Bar with id "foo".
_binder.Bind<Foo>().To<Bar>().ToId("foo").Conclude();
Note
There are other ways to establish an IBinding. Please check the Cheat Sheet for more information.
IResolver
The IResolver is responsible for finding or creating an object and resolving its dependency according to the respective bindings that we registered in the IInstaller using the IBinder. The IResolver enables an instance of a contract type (with an optional id
) to be retrieved as long as the corresponding IBinding exists.
Note
In the case that the corresponding IBinding doesn't exist (has not been bound), a BindingException
would be thrown.
When Resolve
is called with a type information and an optional id, the IResolver looks into the IBinding registry created by the IBinder. If an IBinding matching the type
and id
are found, the IProvider associated with the IBinding provides an instance for the IResolver to return.
Using IResolver to look for the IBinding that is associated with the type Foo
and id
.
//Gets an instance based on the binding with type = Foo and id = null.
Foo bar = _resolver.Resolve<Foo>();
//Gets an instance based on the binding with type = Foo and id = "foo".
Foo foo = _resolver.Resolve<Foo>("foo");
Note
Before the IResolver returns the instance, the IContainer utilizes the IInjector to inject all dependencies into the instance if the instance implements the IInjectable. However, it is valid to bind objects which are not IInjectable.
IInjector
IInjector is a component that lives in the IContainer that is responsible for injecting dependencies recursively into the IInjectable. It is not visible to the users. Please see the example below of the IResolver being injected into the Stage.cs
, which is an IInjectable.
Note
This is how all Entities within IdleKit are dynamically bound at initialization of the game.
IInjectable
When an instance is being resolved in the container by the IResolver, it would have its dependency injected via the void Inject(IResolver resolver)
method if the instance implements the IInjectable interface.
Warning
It is important that the resolver
passed into the Inject
method is never cached so it can only be used in the scope of the method. In addition, void Inject(IResolver)
should only be used to inject the dependencies and absolutely no logic should be performed in the method.
//The IActionService is first bound to the container in the IKInstaller.
public class IKInstaller : IInstaller
{
...
protected virtual void BindServices()
{
_binder.Bind<IActionService>().To<ActionService>().AsSingleton().Conclude();
...
}
}
//When the Stage is loaded, IActionService is injected into the Stage with the IResolver.
public class Stage : IStage, IInjectable
{
...
public virtual void Inject(IResolver resolver)
{
_actionService = resolver.Resolve<IActionService>();
...
}
}
Warning
void Inject(IResolver)
is only called if the IInjectable is obtained using the IResolver. You should only resolve using the IResolver and never directly invoke the constructors while working with the dependency container.
IStage stage = new Stage("instanceId", "staticId"); // No injection will be done.
IStage stage = _resolver.Resolve<IStage>("instanceId"); // Injection will be done before the IStage is returned.
Note
For more information on how an IEntity such as IStage is loaded, please visit the Understanding Entities page.
IBinding
The IBinding makes up the fundamentals of the container. An IBinding allows mapping (binding) of a specific contract type (interface, class) to an IProvider that provides the instance for the type. When an object of a specific type is requested, the IResolver uses the information stored in the IBinding and the IProvider to find or create, and then return an instance of the requested type.
The IBinding also contains an optional id
field that is used for identification between different IBinding of the same contract type. Please check the Cheat Sheet for more information.
Fulfilling Contract
There are four ways to fulfill the contract (binding) type in IdleKit.
public interface IContract { }
public class TContract : IContract { }
//The user does not have to specify anything if the binding type is concrete.
_binder.Bind<TContract>().Conclude();
//Fulfilling the contract type with a concrete implementation of its derived type.
_binder.Bind<IContract>().To<TContract>().Conclude();
//Fulfilling the contract type with an instance of its derived type.
_binder.Bind<IContract>().ToInstance(new TContract()).Conclude();
//Fulfilling the contract type with a method producing an instance of its derived type.
_binder.Bind<IContract>().FromMethod(() => new TContract()).Conclude();
//Fulfilling the contract type with information provided by another provider.
IBinding binding = _binder.Bind<IContract>().To<TContract>().Conclude();
_binder.Bind<IContract>().FromProvider(binding.provider).Conclude();
Lifestyle
The lifestyle
property of the IBinding denotes the scope of the instance returned by the IProvider in the IBinding. There are currently three types of lifestyle a IBinding can have:
Transient
Binding as Transient means that the TContract
returns a new instance of a dependency with a contract type TContract
every time it is resolved. This means that a Transient IBinding
should always be bound by TContract
instead of by ID, as the IDs will always be ignored and a new instance is always returned when resolving.
Cached
Binding as Cached means that the TContract
returns the cached instance of a dependency with a contract type TContract
when the TContract
is resolved. This means that a Cached IBinding
should always be both bound and resolved based on a unique ID. Resolving by TContract
without specifying an ID returns an IBinding
with an ID of null.
Singleton
A Singleton is a specific type of Cached IBinding
which only allows a single instance of type TContract
to be bound to the container. This means that a Singleton IBinding
should always be bound and resolved by type TContract
to retrieve the correct instance. A Singleton IBinding
has an ID of null, and binding a new Singleton IBinding
with type TContract
invalidates all previous IBinding
s of the same contract type TContract
.
The following diagram shows how to decide what type of lifestyle your TContract
should be bound as.
The following table shows how each type of binding should be bound and resolved.
Binding Type | Bind as... | Resolve as... |
---|---|---|
Transient | TContract | TContract |
Cached | TContract + ID | TContract + ID |
Singleton | TContract | TContract |
IProvider
When properly configured, each IBinding has an associated IProvider. The primary function of IProvider is to provide an instance of the contract type for the IBinding that is associated with this IProvider. There are currently four concrete implementations of IProvider:
- CachedProvider - Requires a provider (of the types listed below). Invoking
GetInstance()
returns an instance using that provider. It also caches the object so that the same object is returned whenGetInstance()
is called again, instead of re-injecting the object. - InstanceProvider - Requires a contract type and object. Invoking
GetInstance()
returns the object. A second call toGetInstance()
returns the same object but re-injects it again. - MethodProvider - Requires a contract type and function. Invoking
GetInstance()
returns a new instance using the function - TransientProvider - Requires a contract type and concrete type. Invoking
GetInstance()
returns a new instance of the concrete type
Note
For examples on bindings with different lifestyles using different IProviders, please refer to the Cheat Sheet.