Kategoriak: All - method - injection - dependencies

arabera Mike Ton 4 years ago

1280

Zenject

Constructor and method injection are recommended over field or property injection for several reasons. They offer better portability, making it easier to reuse code without depending on a specific dependency injection framework like Zenject.

Zenject

Zenject

Library

Creating Objects Dynamically Using Factories
Binding Syntax

Container.BindFactory() .WithId(Identifier) .WithFactoryArguments(Factory Arguments) .To() .FromConstructionMethod() .AsScope() .WithArguments(Arguments) .OnInstantiated(InstantiatedCallback) .When(Condition) .NonLazy() .(Copy|Move)Into(All|Direct)SubContainers();

Note that unlike for non-factory bindings, the default is AsCached instead of AsTransient, which is almost always what you want for factories, so in most cases you can leave this unspecified

WithFactoryArguments

Note that WithArguments applies to the actual instantiated type and not the factory

If you want to inject extra arguments into your placeholder factory derived class, you can include them here

PlaceholderFactoryType

The class deriving from PlaceholderFactory<>

The contract type returned from the factory Create method

// Is this the same as Interface?

Theory

Example

Recommended Zenject

public class Player{} public class Enemy{ readonly Player _player; public Enemy(Player player){ _player = player; } public class Factory : PlaceholderFactory{} } public class EnemySpawner : ITickable{ readonly Enemy.Factory _enemyFactory; public EnemySpawner(Enemy.Factory enemyFactory){ _enemyFactory = enemyFactory; } public void Tick(){ if (ShouldSpawnNewEnemy()){ var enemy = _enemyFactory.Create(); // ... } } } public class TestInstaller : MonoInstaller{ public override void InstallBindings(){ Container.BindInterfacesTo().AsSingle(); Container.Bind().AsSingle(); Container.BindFactory(); } }

The way that the object is created is declared in an installer in the same way it is declared for non-factory dependencies

For example, if our Enemy class was a MonoBehaviour on a prefab, we could install it like this instead

public class Enemy : MonoBehaviour{ Player _player; // Note that we can't use a constructor anymore since we are a MonoBehaviour now [Inject] public void Construct(Player player) { _player = player; } public class Factory : PlaceholderFactory{ } } public class TestInstaller : MonoInstaller{ public GameObject EnemyPrefab; public override void InstallBindings(){ Container.BindInterfacesTo().AsSingle(); Container.Bind().AsSingle(); Container.BindFactory

Enemy.Factory is always intentionally left empty and simply derives from the built-in Zenject PlaceholderFactory<> class, which handles the work of using the DiContainer to construct a new instance of Enemy. It is called PlaceholderFactory because it doesn't actually control how the object is created directly.

You could install it like this instead, and bypass the need for a nested factory class:

However, this comes with several drawbacks:

It's less verbose. Injecting Enemy.Factory everywhere is much more readable than PlaceholderFactory, especially as the parameter list grows.

Changing the parameter list is not detected at compile time. If the PlaceholderFactory is injected directly all over the code base, when we add a speed parameter and therefore change it to PlaceholderFactory, then we will get runtime errors when zenject fails to find the PlaceholderFactory dependency (or validation errors if you use validation). However, if we use a derived Enemy.Factory class, then if we later decide to derive from PlaceholderFactory instead, we will get compiler errors instead (at every place that calls the Create method) which is easier to catch

Container.BindFactory>()

There is no requirement that the Enemy.Factory class be a nested class within Enemy

We can also add runtime parameters to our factory. For example, let's say we want to randomize the speed of each Enemy to add some interesting variation to our game. Our enemy class becomes:

public void Tick(){...}

if (ShouldSpawnNewEnemy()) { var newSpeed = Random.Range(MIN_ENEMY_SPEED, MAX_ENEMY_SPEED); var enemy = _enemyFactory.Create(newSpeed); // ... }

public class Factory : PlaceholderFactory{}

The dynamic parameters that are provided to the Enemy constructor are declared by providing extra generic arguments to the PlaceholderFactory<> base class of Enemy.Factory. PlaceholderFactory<> contains a Create method with the given parameter types, which can then be called by other classes such EnemySpawner

from

public class Factory : PlaceholderFactory< Enemy>{}

By using Enemy.Factory above instead of new Enemy, all the dependencies for the Enemy class (such as the Player) will be automatically filled in.

Target

public class Enemy{ Player _player; public Enemy(Player player) { _player = player; } public void Update(){ ... WalkTowards(_player.Position); ... } }

anti-pattern / Service Locator Pattern

public class Enemy{ DiContainer Container; public Enemy(DiContainer container){ Container = container; } public void Update(){ ... var player = Container.Resolve(); WalkTowards(player.Position); ... etc. } }

Important part of dependency injection is to reserve use of the container to strictly the "Composition Root Layer"

factories and installers make up what we refer to as the "composition root layer"

Ensure that new dynamic object gets injected with dependencies just like all the objects that are part of the initial object graph

MonoBehaviours

Note that for dynamically instantiated MonoBehaviours (for example when using FromComponentInNewPrefab with BindFactory) injection should always occur before Awake and Start, so a common convention we recommend is to use Awake/Start for initialization logic and use the inject method strictly for saving dependencies (ie. similar to constructors for non-monobehaviours)

https://github.com/svermeulen/Extenject/blob/master/Documentation/Factories.md
Zenject Settings
Using Zenject Outside Unity Or For DLLs
Runtime Parameters For Installers
General Guidelines / Recommendations / Gotchas / Tips and Tricks
Binding
Scene Bindings
Object Graph Validation
Every dependency injection framework is ultimately just a framework to bind types to instances

DiContainer

C# Native Examples

BindInterfacesAndSelfTo

BindInterfacesTo

IDisposable

IInitializable

ITickable

C# Mono Example

Construction Methods

Bind

Full Bind

Container.Bind() .WithId(Identifier) .To() .FromConstructionMethod() .AsScope() .WithArguments(Arguments) .OnInstantiated(InstantiatedCallback) .When(Condition) .(Copy|Move)Into(All|Direct)SubContainers() .NonLazy() .IfNotBound();

IfNotBound

NonLazy

(Copy|Move)Into(All|Direct)SubContainers

Condition

InstantiatedCallback

Arguments

Scope

ConstructionMethod

Identifier

ResultType

ContractType

InterfaceToInstance

Container.Bind().To().AsSingle();

Any class that requires the IBar interface (like Foo) will be given the same instance of type Bar.

Instance

Container.Bind().AsSingle();

AsSingle

This tells Zenject that every class that requires a dependency of type Foo should use the same instance, which it will automatically create when needed

Class

public class Foo { IBar _bar; public Foo(IBar bar) { _bar = bar; } }

In Zenject, dependency mapping is done by adding bindings to something called a container

C# Reflection

[Inject]

It then attempts to resolve each of these required dependencies, which it uses to call the constructor and create the new instance.

When the container is asked to construct an instance of a given type, it uses C# reflection to find the list of constructor arguments, and all fields/properties that are marked with an [Inject] attribute

The container should then 'know' how to create all the object instances in your application, by recursively resolving all dependencies for a given object.

Injection
Recommendations

Best practice is to prefer constructor/method injection compared to field/property injection

Clear dependencies

Finally, Constructor/Method injection makes it clear what all the dependencies of a class are when another programmer is reading the code

his is also good because it will be more obvious when a class has too many dependencies and should therefore be split up (since its constructor parameter list will start to seem long)

They can simply look at the parameter list of the method

Simpler to port

Constructor/Method injection is more portable for cases where you decide to re-use the code without a DI framework such as Zenject

You can do the same with public properties but it's more error prone (it's easier to forget to initialize one field and leave the object in an invalid state)

No circular dependencies

Constructor injection guarantees no circular dependencies between classes, which is generally a bad thing to do

Zenject does allow circular dependencies when using other injections types however such as method/field/property injection

// When would you want his????

Resolve at initialization

Constructor injection forces the dependency to only be resolved once, at class creation, which is usually what you want. In most cases you don't want to expose a public property for your initial dependencies because this suggests that it's open to changing

Notes

With the exception where there is circular dependencies

This can be important if you use inject methods to perform some basic initialization, since in that case you may need the given dependencies to be initialized as well

Note however that it is usually not a good idea to use inject methods for initialization logic

Often it is better to use IInitializable.Initialize or Start() methods instead, since this will allow the entire initial object graph to be created first.

You can safely assume that the dependencies that you receive via inject methods will themselves already have been injected

Method

public class Foo
{
    IBar _bar;
    Qux _qux;

    [Inject]
    public void Init(IBar bar, Qux qux)
    {
        _bar = bar;
        _qux = qux;
    }
}


Inject methods are the recommended approach for MonoBehaviours, since MonoBehaviours cannot have constructors.

Inject methods are called after all other injection types

Note also that you can leave the parameter list empty if you just want to do some initialization logic only.

It is designed this way so that these methods can be used to execute initialization logic which might make use of injected fields or properties.

There can be any number of inject methods. In this case, they are called in the order of Base class to Derived class

This can be useful to avoid the need to forward many dependencies from derived classes to the base class via constructor parameters, while also guaranteeing that the base class inject methods complete first, just like how constructors work.

Property

public class Foo
{
    [Inject]
    public IBar Bar
    {
        get;
        private set;
    }
}


Field

public class Foo
{
    [Inject]
    IBar _bar;
}


Constructor

public class Foo
{
    IBar _bar;

    public Foo(IBar bar)
    {
        _bar = bar;
    }
}


Unity3D

Editor
Edit -> Zenject -> Validate Current Scene
Scene

GameObject

SceneContext.cs

Installers

Prefabs

.gameObject

Mono

.cs

Scriptable

.asset