Skip to content

Commit

Permalink
Add On-Demand Style Injection
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeffan207 committed Aug 25, 2022
1 parent 9a6b7ff commit 8388e41
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 17 deletions.
51 changes: 46 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ The SceneInjection attributes controls whether or not the object will injected w
[SceneInjection(enabled: false)]
public class Food : MonoBehaviour {

protected Brand brand;
private Brand brand;

private void Start() {
this.brand = SyrupComponent.SyrupInjector.Get<Brand>();
Expand Down Expand Up @@ -383,7 +383,7 @@ The Syrup Component itself has the following tunable parameters:

The Syrup Injector lives within the Syrup Component. It is responsible for building the dependency graph and fetching/caching any dependencies as they are needed. It is created on first scene load and is destroyed in `OnDestroy()` when the scene transitions (meaning your dependencies are recreated between scenes). The Syrup Injector is a singleton and any new Syrup Components and Modules added to your scene will be added directly to the currently active Syrup Injector.

If you need to, you can fetch dependencies directly from the Syrup Injector on your Syrup Component via static calls to the `SyrupInjector` property in the `SyrupComponent` class. You can use the Syrup Injector to fetch both named and unnamed dependencies using the `GetInstance<>()` and `GetInstance<>(string name)` methods respectively.
If you need to, you can fetch dependencies directly from the Syrup Injector on your Syrup Component via static calls to the `SyrupInjector` property in the `SyrupComponent` class. You can use the Syrup Injector to fetch both unnamed and named dependencies using the `GetInstance<>()` and `GetInstance<>(string name)` methods respectively.

```c#
//Get's an instance of the TastySyrup object supplied by the Syrup Injector.
Expand All @@ -394,21 +394,62 @@ Milk milk = SyrupComponent.SyrupInjector.GetInstance<>("LactoseFreeMilk")l

```

### Where is Field Injection?
### On-Demand Injection

For convenience, The Syrup Injector can also be used to inject any object on-demand using the `SyrupInjector.Inject(T objectToInject)` API. This is useful for scenarios where the object to be injected is created at runtime and is not available to be injected during the Syrup Component's initial inject on scene load step. It also means for this scenario you can use normal `[Inject]` semantics instead of needing to rely on `SyrupInjector.GetInstance()` calls to fulfill your objects dependencies.

```c#
//This is an example WITHOUT using on-demand injection
public class Breakfast : MonoBehaviour {

private Syrup syrup;
private Pancakes pancakes;
private Bacon bacon;

private void Start() {
this.syrup = SyrupComponent.SyrupInjector.Get<Syrup>();
this.pancakes = SyrupComponent.SyrupInjector.Get<Pancakes>();
this.bacon = SyrupComponent.SyrupInjector.Get<Bacon>();
}
}

//This is an example WITH using on-demand injection
public class Breakfast : MonoBehaviour {

private Syrup syrup;
private Pancakes pancakes;
private Bacon bacon;

private void Start() {
SyrupComponent.SyrupInjector.Inject(this);
}

[Inject]
public void InitBreakfast(Syrup syrup, Pancakes pancakes, Bacon bacon) {
this.syrup = syrup;
this.pancakes = pancakes;
this.bacon = bacon;
}
}
```

The above examples show the `Breakfast` class implemented in two ways. The first way using `SyrupInjector.GetInstance()` to fulfill the `Breakfast` class' dependencies and a second way using `SyrupInjector.Inject(this)`. Using the second method, the SyrupInjector will call back into the `Breakfast` class and invoke it's injectable `InitBreakfast()` method with the dependencies it needs to provide. As can be seen, one way is not strictly better than the other as both can be used to accomplish the same goal.

## Where is Field Injection?

Currently, field injection is not supported. The use-cases for field injection, particularly for MonoBehaviours, can be covered by method injection.

If you have a use-case that contradicts this, feel free to make a request!

### Where is 'X' dependency injection framework feature?
## Where is 'X' dependency injection framework feature?

USyrup is meant to be *SIMPLE*! In the interest of simplicity (and time) only the most basic dependency injection features were implemented. This is a very opinonated viewpoint as I, (the author), only ever really use these features anyway.

If you feel strongly the framework can really benefit from having 'X' feature, feel free to make a request!

My personal take has always been however, "just because a framework/tool provides a number of features doesn't mean you need to or should use *ALL* of them!"

### Why the name "Syrup"?
## Why the name "Syrup"?

Three reasons:

Expand Down
25 changes: 19 additions & 6 deletions SyrupSource/Syrup/Framework/SyrupInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,24 @@ public class SyrupInjector {
return (T)dependency;
}

/// <summary>
/// Injects dependencies into the provided object by invoking any inject related methods.
/// </summary>
/// <param name="objectToInject">The object to be injected</param>
public void Inject<T>(T objectToInject) {
MethodInfo[] injectableMethods = SyrupUtils.GetInjectableMethodsFromType(objectToInject.GetType());
InjectObject(objectToInject, injectableMethods);
}

private void InjectObject<T>(T objectToInject, MethodInfo[] injectableMethods) {
//We're making the assumption that the injectable methods are ordered from base class
//to deriving class (they should be) but we're assuming it too.
foreach (MethodInfo injectableMethod in injectableMethods) {
object[] parameters = GetMethodParameters(injectableMethod);
injectableMethod.Invoke(objectToInject, parameters);
}
}


// BEGIN ALL UNITY SPECIFIC INJECTION PATTERNS

Expand Down Expand Up @@ -379,12 +397,7 @@ public class SyrupInjector {

private void InjectGameObjects(List<InjectableMonoBehaviour> injectableMonoBehaviours) {
foreach (InjectableMonoBehaviour injectableMb in injectableMonoBehaviours) {
//We're making the assumption that the injectable methods are ordered from base class
//to deriving class (they should be) but we're assuming it too.
foreach (MethodInfo injectableMethod in injectableMb.methods) {
object[] parameters = GetMethodParameters(injectableMethod);
injectableMethod.Invoke(injectableMb.mb, parameters);
}
InjectObject(injectableMb.mb, injectableMb.methods);
}
}
}
Expand Down
16 changes: 10 additions & 6 deletions SyrupSource/Syrup/Framework/SyrupUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,19 @@ internal class SyrupUtils {
continue;
}

var injectableMethods = monoBehaviourType.GetMethods()
.Where(x => x.GetCustomAttributes(typeof(Inject), false).FirstOrDefault() != null);
MethodInfo[] methods = injectableMethods.Reverse().ToArray();
if (methods.Length > 0) {
injectableMonoBehaviours.Add(new InjectableMonoBehaviour(monoBehaviour, methods));
var injectableMethods = GetInjectableMethodsFromType(monoBehaviourType);
if (injectableMethods.Length > 0) {
injectableMonoBehaviours.Add(new InjectableMonoBehaviour(monoBehaviour, injectableMethods));
}
}
}

}
}

public static MethodInfo[] GetInjectableMethodsFromType(Type t) {
var injectableMethods = t.GetMethods()
.Where(x => x.GetCustomAttributes(typeof(Inject), false).FirstOrDefault() != null);
return injectableMethods.Reverse().ToArray();
}
}
}
26 changes: 26 additions & 0 deletions Tests/Runtime/Framework/SyrupComponentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,30 @@ public class SyrupComponentTest {
UnityEngine.Object.Destroy(toast);
}

/// <summary>
/// This test probably doesn't belong here.
/// It tests that a MonoBehaviour's Start() can call SyrupComponent.SyrupInjector.Inject(this).
/// </summary>
[UnityTest]
public IEnumerator TestSyrupComponent_WithOnDemandInjection() {
//Create the SyrupComponent first and wait a frame that way it will go through its injection loop
GameObject sceneComponent = new GameObject();
sceneComponent.AddComponent<SyrupComponent>();
yield return null;

//The AutoToast will be injected via on-demand injection
GameObject autoToast = new GameObject();
autoToast.AddComponent<AutoToast>();

//Wait a frame so it goes through its Start() method.
yield return null;

AutoToast autoToastComponent = autoToast.GetComponent<AutoToast>();

Assert.NotNull(autoToastComponent.butter);

UnityEngine.Object.Destroy(autoToast);
UnityEngine.Object.Destroy(sceneComponent);
}

}
30 changes: 30 additions & 0 deletions Tests/Runtime/Framework/SyrupInjectorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,34 @@ public class SyrupInjectorTest {
Assert.NotNull(americanBuffet.tastySyrup);
Assert.NotNull(americanBuffet.egg);
}

[Test]
public void TestSyrupInjector_CanInjectOnDemand() {
SyrupInjector syrupInjector = new SyrupInjector();

EnglishMuffin englishMuffin = new();

Assert.Null(englishMuffin.butter);

syrupInjector.Inject(englishMuffin);

Assert.NotNull(englishMuffin.butter);
}

[Test]
public void TestSyrupInjector_OnDemandInjectionWithInheritedInjects() {
SyrupInjector syrupInjector = new SyrupInjector(
new TwoDependentProvidersModule(),
new SingleProviderModule(),
new ProvidedEggModule());

AmericanBuffet americanBuffet = new();
Assert.Null(americanBuffet.pancake);
Assert.Null(americanBuffet.tastySyrup);
Assert.Null(americanBuffet.egg);
syrupInjector.Inject(americanBuffet);
Assert.NotNull(americanBuffet.pancake);
Assert.NotNull(americanBuffet.tastySyrup);
Assert.NotNull(americanBuffet.egg);
}
}
23 changes: 23 additions & 0 deletions Tests/Runtime/Framework/TestData/AutoToast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using UnityEngine;
using System.Collections;
using Syrup.Framework.Attributes;


/// <summary>
/// This toast injects itself!
/// </summary>
namespace Tests.Framework.TestData {
public class AutoToast : MonoBehaviour {

public Butter butter;

private void Start() {
SyrupComponent.SyrupInjector.Inject(this);
}

[Inject]
public void Init(Butter butter) {
this.butter = butter;
}
}
}
11 changes: 11 additions & 0 deletions Tests/Runtime/Framework/TestData/AutoToast.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions Tests/Runtime/Framework/TestData/EnglishMuffin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using Syrup.Framework.Attributes;
using Tests.Framework.TestData;

namespace Tests.Framework.TestData {
[Singleton]
public class EnglishMuffin : Identifiable {

public Butter butter;

[Inject]
public void Init(Butter butter) {
this.butter = butter;
}

}
}
11 changes: 11 additions & 0 deletions Tests/Runtime/Framework/TestData/EnglishMuffin.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8388e41

Please sign in to comment.