von Mike Ton Vor 4 Jahren
1308
Mehr dazu
Container.BindFactory
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?
Example
Recommended Zenject
public class Player{}
public class Enemy{
readonly Player _player;
public Enemy(Player player){
_player = player;
}
public class Factory : PlaceholderFactory
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
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
Changing the parameter list is not detected at compile time. If the PlaceholderFactory
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
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"
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)
DiContainer
C# Native Examples
BindInterfacesAndSelfTo
BindInterfacesTo
IDisposable
IInitializable
ITickable
C# Mono Example
Construction Methods
Bind
Full Bind
Container.Bind
IfNotBound
NonLazy
(Copy|Move)Into(All|Direct)SubContainers
Condition
InstantiatedCallback
Arguments
Scope
ConstructionMethod
Identifier
ResultType
ContractType
InterfaceToInstance
Container.Bind
Any class that requires the IBar interface (like Foo) will be given the same instance of type Bar.
Instance
Container.Bind
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.
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
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
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.
public class Foo { [Inject] public IBar Bar { get; private set; } }
public class Foo { [Inject] IBar _bar; }
public class Foo { IBar _bar; public Foo(IBar bar) { _bar = bar; } }
GameObject
SceneContext.cs
Installers
Prefabs
.gameObject
Mono
.cs
Scriptable
.asset