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 when GetInstance()
is called again, instead of re-injecting the object. * InstanceProvider - Requires a contract type and object. Invoking GetInstance()
returns the object. A second call to GetInstance()
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.