Skip to content

natpuncher/bindlessdi

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

94 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PRs Welcome

bindlessdi

Lightweight dependency injection framework for Unity almost free of bindings designed to simplify and streamline the process of writing code.

Installation

  • In Package Manager press +, select Add package from git URL and paste https://github.com/natpuncher/bindlessdi.git
  • Or find the manifest.json file in the Packages folder of your project and add the following line to dependencies section:
{
 "dependencies": {
    "com.npg.bindlessdi": "https://github.com/natpuncher/bindlessdi.git",
 },
}

Usage

Initializing the Container

To initialize Bindlessdi call Container.Initialize() from entry point of the game.

public class EntryPoint : MonoBehaviour
{
    private void Start()
    {
        var container = Container.Initialize();

        var myGame = container.Resolve<MyGame>();
        myGame.Play();
    }
}

Injecting

Bindlessdi only supports constructor injection.

public class MyGame
{
    private readonly MyController _myController;

    public MyGame(MyController controller)
    {
        _myController = controller;
    }
}

In most cases Bindlessdi will guess implementation by itself without any bindings.

public class MyController : IController
{
}
public class MyGame
{
    private readonly IController _controller;

    public MyGame(IController controller)
    {
        _controller = controller;
    }
}

If there are several of implementations for specific interface, intended implementation should be defined by calling container.BindImplementation<IInterface, Implementation>().

public class EntryPoint : MonoBehaviour
{
    private void Start()
    {
        var container = Container.Initialize();
        container.BindImplementation<IBar, Qux>();

        var myGame = container.Resolve<MyGame>();
        myGame.Play();
    }
}
public class Foo : IFoo
{
}

public class Bar : IBar
{
}

public class Qux : IBar
{
}
public class MyGame
{
    public MyGame(IFoo foo, IBar bar)
    {
        // bar is Qux
    }
}

Bind Instance

Already instanced objects could be added to container by calling container.BindInstance(instance). Its also possible to add a bunch of them using container.BindInstances(instances).

public class EntryPoint : MonoBehaviour
{
    private void Start()
    {
        var container = Container.Initialize();

        var myController = new MyController();
        container.BindInstance(myController);

        var guns = new IGun[] { new FireGun(), new ColdGun() };
        container.BindInstances(guns);

        var myGame = container.Resolve<MyGame>();
        myGame.Play();
    }
}
public class MyGame
{
    public MyGame(MyController myController, FireGun fireGun, ColdGun coldGun)
    {
    }
}

Instantiation Policy

Bindlessdi resolves everything as single instance by default, but it can be changed.

  • By changing default initialization policy to InstantiationPolicy.Transient, so every resolve will create a new instance.
public class EntryPoint : MonoBehaviour
{
    private void Start()
    {
        var container = Container.Initialize();
        container.DefaultInstantiationPolicy = InstantiationPolicy.Transient;

        var myGame = container.Resolve<MyGame>();
        myGame.Play();

        var myGame2 = container.Resolve<MyGame>();
        myGame2.Play();

        // myGame != myGame2
    }
}
  • By registering instantiation policy for concrete type, so every resolve of this type will create a new instance.
public class EntryPoint : MonoBehaviour
{
    private void Start()
    {
        var container = Container.Initialize();
        container.RegisterInstantiationPolicy<MyGame>(InstantiationPolicy.Transient);

        var myGame = container.Resolve<MyGame>();
        var myGame2 = container.Resolve<MyGame>();
        // myGame != myGame2
    }
}
  • By registering instantiation policy for an interface or a base type, so every resolve of a type implementing it will get this policy override.
public class EntryPoint : MonoBehaviour
{
    private void Start()
    {
        var container = Container.Initialize();
    
        container.DefaultInstantiationPolicy = InstantiationPolicy.Single;
        container.RegisterInstantiationPolicy<ITrigger>(InstantiationPolicy.Transient);
        container.RegisterInstantiationPolicy<TriggerA>(InstantiationPolicy.Single);
		
        var a1 = container.Resolve<TriggerA>();
        var a2 = container.Resolve<TriggerA>();
        // a1 == a2
		
        var b1 = container.Resolve<TriggerB>();
        var b2 = container.Resolve<TriggerB>();
        // b1 != b2
    }
}
  • By passing instantiation policy to container.Resolve() method, so only this call will create a new instance.
public class EntryPoint : MonoBehaviour
{
    private void Start()
    {
        var container = Container.Initialize();

        var myGame = container.Resolve<MyGame>();
        var myGame2 = container.Resolve<MyGame>(InstantiationPolicy.Transient);
        var myGame3 = container.Resolve<MyGame>(InstantiationPolicy.Transient);
        var myGame4 = container.Resolve<MyGame>();
        // myGame != myGame2 != myGame3
        // myGame == myGame4
    }
}

Factory

To create instances on demand use Factories for a specific interface. Just add IFactory<MyInterface> as a constructor argument and call Resolve with concrete type when needed. It is also possible to override instantiation policy on resolve. There is no need to bind Factories, they will be resolved automatically.

public interface IBullet
{
}

public class FireBullet : IBullet
{
}

public class ColdBullet : IBullet
{
}
public class Gun
{
    private readonly IFactory<IBullet> _factory;

    public Gun(IFactory<IBullet> factory)
    {
        _factory = factory;
    }

    public IBullet Fire()
    {
        return _factory.Resolve<FireBullet>(InstantiationPolicy.Transient);
    }

    public IBullet Cold()
    {
        return _factory.Resolve<ColdBullet>(InstantiationPolicy.Transient);
    }
}

Working with Unity Objects

  • Create an implementation of SceneContext class
public class MyGameSceneContext : SceneContext
{
    public override IEnumerable<Object> GetObjects()
    {
        return new Object[] { };
    }
}
  • Create a GameObject in the root of the scene and attach the SceneContext implementation script to it

  • Add [SerializeField] private fields for links to Components from scene, Prefabs or Scriptable Object assets and return it from GetObjects() method

public class MyGameSceneContext : SceneContext
{
    [SerializeField] private Camera _camera;
    [SerializeField] private MyHudView _hudView;
    [SerializeField] private BulletView _bulletPrefab;
    [SerializeField] private MyScriptableObjectConfig _config;

    public override IEnumerable<Object> GetObjects()
    {
        return new Object[] { _camera, _hudView, _bulletPrefab, _config };
    }
}
  • Get UnityObjectContainer class as a constructor argument and receive objects by calling unityObjectContainer.TryGetObject(out TObject object) method
public class MyGame
{
    private readonly UnityObjectContainer _unityObjectContainer;
    
    public MyGame(UnityObjectContainer unityObjectContainer)
    {
        _unityObjectContainer = unityObjectContainer;
    }
    
    public void Play()
    {
        if (!_unityObjectContainer.TryGetObject(out MyHudView hudView) ||
            !_unityObjectContainer.TryGetObject(out MyScriptableObjectConfig config))
        {
            return;
        }

        hudView.Show(config);
    }
}

Unity Events

Implement ITickable, IFixedTickable, ILateTickable, IPausable, IDisposable to handle Unity Events. There is no need to bind these interfaces to a class, once instance will be resolved - Unity Events will be passed to it.

Unity Update => ITickable
Unity FixedUpdate => IFixedTickable
Unity LateUpdate => ILateTickable
Unity Pause => IPausable
Unity Application.Quit => IDisposable

public class MyGame : ITickable, IDisposable
{
    public void Tick()
    {
        // called on Unity Update
    }

    public void Dispose()
    {
        // called on Application.Quit
    }
}

This functionality could be turned off by passing false to Container initialization

public class EntryPoint : MonoBehaviour
{
    private void Start()
    {
        var container = Container.Initialize(false);

        var myGame = container.Resolve<MyGame>();
        myGame.Play();
    }
}
public class MyGame : ITickable, IDisposable
{
    public void Tick()
    {
        // not called
    }

    public void Dispose()
    {
        // not called
    }
}

Optimizations

By default, Bindlessdi will search for certain types and cache their implementations whenever it needs to guess them, but this process can be optimized.

  • By calling Container.WarmupImplementationCache behind the loading screen of your game. This will force Bindlessdi to collect and cache all needed information in the right time if you want to use Bindlessdi without bindings.
  • Or by binding all of your implementations by calling Container.BindImplementation, so there will be no need for Bindlessdi to find implementations by itself.

Tests

To use Bindlessdi in tests call Container.Initialize(false) in the begining of a test and container.Dispose in the end.

public class MyTests
{
    [Test]
    public void TestResolve()
    {
            var container = Container.Initialize(false);

            var a = container.Resolve<A>();
            var b = container.Resolve<B>();
            var c = container.Resolve<C>();
            var d = container.Resolve<D>();

            Assert.True(a.B == b);
            Assert.True(a.C == b.C);
            Assert.True(a.C == c);
            Assert.True(b.C == c);
            Assert.True(b.D == c.D);
            Assert.True(b.D == d);

            container.Dispose();
    }
}