Will Corwin strikes again, offering a powerful new feature to the Strange feature set. This time, we introduce ‘Promises’, a simple, powerful pattern for handling asynchronous callback behavior. If you’re familiar with q-promise (or the AngularJS equivalent), you’ll know exactly where we’re going with this.
[EDIT] June 8. Attentive reader Stephan identified that I had oversimplified my examples by forgetting to inject RoutineRunner. I’ve quickly added in the missing bits — early on a Monday morning — hopefully I repaired it all correctly![/EDIT]
This will sound crazy at first (bear with me for a moment) inasmuch as we’ve always told you about dependency inversion, but with promises we will now “invert the inversion”. That does sound crazy doesn’t it? But it actually makes sense! We practice inversion because the proper job of setting dependencies lies outside the client class. Promises turn this on its head, because the responsibility for determining what to do with a job the client requested lies with the client itself.
“I went to the bakery and ordered a cake.” The baker doesn’t know why you asked (though you might add some detail so he can decorate), nor should he. Decoupling in this case means both client and service know as little about each other as possible. Client calls service’s API, asking for the “cake”. Perhaps a cake is already available. Perhaps not.
The baker/service responds with a promise to deliver the cake now, or whenever it’s ready. Responding with a promise for a thing instead of the thing itself allows greater flexibility whenever you know or suspect that the thing might not be instantly available.
Let’s take this from the theoretical to the concrete with something everyone is certainly familiar with: an HTTP call.
We all know this would fail:
string GetLevelName() { return new WWW('http://myserver/get/the/level'); }
This fails because this isn’t how WWW works. WWW doesn’t have the result sitting around for you. It has to go to a server to get it, and that takes time. To handle this, we write WWW with a yield:
IEnumerator FetchLevelName() { WWW www = new WWW('http://myserver/get/the/level'); yield return www; CurrentLevel = www.text; //Now do something with the level name }
So in a spaghetti-coded world, this is fine. You could drop this into any MonoBehaviour and be off to the races. But we’re in Strange, and that means we like to separate this sort of behavior into a service. So imagine a Command that triggers this service and wants to hang around until the result is ready. How do we do this?
Note that throughout the following examples we use the same RoutineRunner you can find in StrangeRocks.
// The Command using System; using strange.extensions.command.impl; namespace strange.test { public class FetchLevelNameCommand : Command { [Inject] public IServerAPI service { get; set;} override public void Execute() { Retain(); service.FetchLevelName(); } } } // The Service using System; using System.Collections; namespace strange.test { public class ServerAPI : IServerAPI { [Inject] public IRoutineRunner routineRunner { get; set; } private string CurrentLevel; public void FetchLevelName() { routineRunner.StartCoroutine(Fetch); } private IEnumerator Fetch() { WWW www = new WWW('http://myserver/get/the/level'); yield return www; CurrentLevel = www.text; //Now do something with the level name } } }
In the code above, I’ve obviously left a great big hole. The Command is Retained, waiting for the Service to return a value. But there’s not yet any way for the Command to get the data back, finish its business and release. Since the yield is inside the Service, there’s no real way for the Command to know when the data it’s awaiting is ready.
We could handle this by polling the service:
// The Command using System; using System.Collections; using UnityEngine; using strange.extensions.command.impl; namespace strange.test { public class FetchLevelNameCommand : Command { [Inject] public IServerAPI service { get; set;} [Inject] public IRoutineRunner routine { get; set; } override public void Execute() { Retain(); routine.StartCoroutine(CheckForName()); service.FetchLevelName(); } private IEnumerator CheckForName() { yield return new WaitForSeconds(.2f); if (service.CurrentLevel != null) { //Finally do something with the data Release(); } else { routine.StartCoroutine(CheckForName()); } } } } // The Service using System; using System.Collections; namespace strange.test { public class ServerAPI : IServerAPI { [Inject] public IRoutineRunner routineRunner { get; set; } public string CurrentLevel { get; set; } public void FetchLevelName() { routineRunner.StartCoroutine(Fetch); } private IEnumerator Fetch() { WWW www = new WWW('http://myserver/get/the/level'); yield return www; CurrentLevel = www.text; } } }
Ok, that works for a definition of “works” that means “ugly, inefficient and unmaintainable.” It requires that the client knows a lot more about the inner workings of the service than you would ever want.
We can improve on this with a signal from the service.
// The Command using System; using strange.extensions.command.impl; namespace strange.test { public class FetchLevelNameCommand : Command { [Inject] public IServerAPI service { get; set;} override public void Execute() { Retain(); service.NameReadySignal.AddOnce(OnName); service.FetchLevelName(); } private void OnName(string name) { //Do something with the data Release(); } } } // The Service using System; using System.Collections; using strange.extensions.signal.impl namespace strange.test { public class ServerAPI : IServerAPI { [Inject] public IRoutineRunner routineRunner { get; set; } private string CurrentLevel; public Signal NameReadySignal = new Signal(); public void FetchLevelName() { routineRunner.StartCoroutine(Fetch); } private IEnumerator Fetch() { WWW www = new WWW('http://myserver/get/the/level'); yield return www; CurrentLevel = www.text; NameReadySignal.Dispatch(CurrentLevel); } } }
This a substantial improvement. The service has a defined API, which the client accesses. When the data is ready, the Service fires its signal and the client is informed.
The promise pattern is similar, but it trims the API considerably.
// The Command using System; using strange.extensions.command.impl; namespace strange.test { public class FetchLevelNameCommand : Command { [Inject] public IServerAPI service { get; set;} override public void Execute() { Retain(); service.FetchLevelName().Then(OnName); } private void OnName(string name) { //Do something with the data Release(); } } } // The Service using System; using System.Collections; using strange.extensions.promise.api; using strange.extensions.promise.impl; namespace strange.test { public class ServerAPI : IServerAPI { [Inject] public IRoutineRunner routineRunner { get; set; } private IPromise promise = new Promise(); public IPromise FetchLevelName() { routineRunner.StartCoroutine(FetchLevelNameFromServer); return promise; } private IEnumerator FetchLevelNameFromServer () { WWW www = new WWW('http://myserver/get/the/level'); yield return www; promise.Dispatch(www.text); } } }
Note how clean the Promise structure is from outside the service. Just a simple promise.Then().
One beautiful aspect of this pattern is that the promise can be (but does not have to be) resolved immediately. So imagine we now cache our server data. This change means that the data may or may not be local at any given moment. The Promise handles this possibility very elegantly (note changes in red):
// The Command using System; using strange.extensions.command.impl; namespace strange.test { public class FetchLevelNameCommand : Command { [Inject] public IServerAPI service { get; set;} override public void Execute() { Retain(); service.FetchLevelName().Then(OnName); } private void OnName(string name) { //Do something with the data Release(); } } } // The Service using System; using System.Collections; using strange.extensions.promise.api; using strange.extensions.promise.impl; namespace strange.test { public class ServerAPI : IServerAPI { [Inject] public IRoutineRunner routineRunner { get; set; } private string CurrentLevel; private IPromise promise = new Promise(); public IPromise FetchLevelName() { if (CurrentLevel != null) { promise.Dispatch(CurrentLevel); } else { routineRunner.StartCoroutine(FetchLevelNameFromServer); } return promise; } private IEnumerator FetchLevelNameFromServer () { WWW www = new WWW('http://myserver/get/the/level'); yield return www; CurrentLevel = www.text; promise.Dispatch(CurrentLevel); } } }
See how that change is completely invisible to the client? Whether that data is available or not is no longer the client’s responsibility. The contract between promisor and promisee is “I’ll deliver it when it’s ready.” That’s all.
The promise structure is very simple and flexible, every promise method returns a promise. In the same way that a binder can Bind.To.To.To… a promise can Promise.Then.Then.Then… allowing you to create chains of resulting actions.
There are also methods for Progress, Fail and Finally which you can use for reporting percentage of job completion, errors and irrespective conclusion, respectively.
Promise is yet another new feature in the upcoming next release of StrangeIoC, now available on the tip branch. For the full set of planned features see this wiki page. As always, we encourage you to test out the new features and help us find any problems before we make this build official.
Thanks!