Promises, promises

Pinky PromiseWill 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!

Advertisements

Is Strange Right for You?

Ask your Doctor

A user on our forum — going through the teething pains most of us experience when learning IoC — asked us about Strange’s suitability for use with Unity. I was happy to oblige with something of an empirical defense: look at our user base, all the studios, all the cred we have inside and outside Unity. Of course Strange is suitable!

But still, I conceded, suitability for some cases doesn’t automatically imply that Strange is right for him or his project. It’s not a one-size-fits-all question. Rather, the answer lies somewhere on a continuum. So how do you know if Strange is the tool for you and your project? So glad you asked!

Strange has several advantages — and a few disadvantages. The more your intended project plays into these strengths, the more likely you would want to apply it. And even when applying it, a sensible understanding of the weaknesses should help you understand exactly where in your product Strange is best applied. In this post, I want to suggest some metrics by which you can make these decisions, none of which will be affected by the upcoming release of StrangeIoC 1.0.

Complexity (yours)

Perhaps more than anything, Strange is a tool for dealing with code complexity. Is your project likely to be complex? If I’m throwing together a quick example, or building Tic-Tac-Toe, I’m probably not going to be motivated to use Strange. The bigger my project, though, the more likely I will want something to handle my dependencies in an elegant manner.

Complexity (ours)

There can be no denying that Strange has a learning curve. For some, it’s pretty steep. To use Strange, you and your team need to learn to use it. They need to understand not just how Strange works, but why. The end benefits are pretty impressive, but only if they’re used according to the manufacturing instructions. If you’re just learning how to code, it might be good enough simply to know that we exist. Learn the basics and come back when you start to understand why code decoupling is a problem you want to solve. Similarly, if you doubt your team’s ability to circle up around a framework, don’t worry. Just put the blame on us.

Programming AbilityTeam Size & Complexity

Team size is also an issue of code complexity, with a slightly different twist. In this case, we’re thinking about the myriad styles in which different coders code. This difficulty can be mitigated with coding standards, but just as every writer has a distinct voice and syntax, so every coder implements ideas in her own inimitable style. That’s great up to a point. But when you have a large team and a tight deadline, ironing out these differences such that I can jump into your code and you can jump into mine can be the difference between making a deadline or not. Opinionated  frameworks like Strange tend to urge you to code in a particular way, leading to greater homogeneity. For lots of common problems, the architectural solutions become obvious and automatic, so you don’t need to spend time thinking about them. When you move from class-to-class (or even from project-to-project!) the code looks familiar and you can get right into it with minimal learning.

Additionally, Strange’s management of multiple Contexts means that teams or team members can break a project into sub-modules. Each module can be built independently, then joined up near the end of the development process.

Team Size & ComplexityVolatility

Another key selling point for Strange is its inherent resilience to change. A simple project is unlikely to change much. A real product operating under real world circumstances (with customers, producers, product managers, data analysts, etc. all urging you for fixes, new features and enhancements) might alter considerably. And these alterations may not be predictable or under your control.

The more volatile your project and environment, the greater the odds that you need the ability to adapt.

Code VolatilityPerformance

“How much performance do you need?” The knee-jerk reaction, naturally, is “all of it”, and that’s fair enough. Strange manages performance hits related to reflection, but even with that, there’s always going to be modest overhead related to requesting an injection. Usually, this is tiny when compared with driving your visuals or running an AI algorithm, but even so, we, like you, want to minimize overheads. For this reason we typically suggest keeping your Strange-related activities to a minimum in your most performance-hungry loops.

So be realistic about your performance needs. In a turn-based game or even for occasional events in a shooter, Strange’s overhead is negligible. The nearer your game approaches the realm of performance junkie, however, the more you want to limit its activities.

Performance requirementsTestability

Unit testing is a huge win…some of the time. It can also be a burdensome time-sink. Which one it is for you is a matter of experience, expertise, and personal preference. Generally, I find that testing my models and services is advisable. Testing controllers depends on complexity. Views are often hard or impossible to test. Whatever your opinion, if you value testing at all then offloading code away from MonoBehaviours makes unit testing possible. You don’t need Strange to do this, but it sure helps.

Code testabilityIntegration with other Assets

I’ve not had much trouble with this, but it’s possible you might. One of the very first wins I personally got from using Strange was isolating nGUi into a handful of View classes, thereby keeping it away from everything else. When uGui came along, refactoring was a breeze. Some assets unsurprisingly employ tactics that Strange philosophically opposes, such as using Singletons or attaching non-View logic to MonoBehaviours. Again, for me this has been a net win for Strange, because I can isolate those “bad” practices, wrap them in an interface, and keep them from infecting my code base. If I someday decided to swap one asset for another, I have minimal refactoring to do, since I never let them get out of control.

Now, I can’t guarantee that you’ll never run across an asset that doesn’t play well with Strange. Let’s assume you do. In that very rare case, you may need to shoehorn some logic in a weird, crazy patch. Disaster? Not in my book. If 90% of my code is well-managed, I’ll gladly accept 10% patched together. I’m still beating the odds handily.

Plays well with other assetsPlatforms

Unity deploys to an awful lot of platforms. Strange hasn’t been tested on all of them. We’re good for the obvious stuff (iOS, Android, standalone and web). Follow this link for the complete list of platforms where we’ve validated our framework.

Are we Strange yet?

As you can see, Strange offers many advantages as your code gets complex and volatile. Among these are insulation, flexibility, modularity, and testability. It also comes with a learning curve and some very modest performance costs. So is Strange right for you? I think you’re ready to answer for yourself.

Owning my responsibilities

Today’s post is not precisely about Strange so much as about keeping one’s eye the fundamentals.

As you might already know, we’ve been working on adding editor functionality so that you can create StrangeIoC-based Extensions for Unity. I usually approach a new project like this in a few simple steps:

  1. Quick and dirty proof of concept
  2. Refactor and tighten
  3. Test and correct

As you can see I’m not really a TDD guy, though I end up with a similar product.

In step one, I copied and pasted the MediationBinder and hacked a version that would work with EditorWindows. This led, in step two, to an obvious refactor: extracting commonalities between MediationBinder and the new EditorMediationBinder into an AbstractMediationBinder. And Oh Look! all those commonalities – freed from the surly shackles of MonoBehaviours – reveal big chunks of testable code!

This has always been an Achilles’ Heel for Strange. The untestability of the mediation package has meant that we have never been entirely certain of the quality of our code in this part of the framework. Because of this, we necessarily approach changes here with a lot more caution then we might do any other part of Strange.

My buddy Will saw this and immediately went to town with a further refactor. The result is amazing: virtually the whole of the (Abstract)MediationBinder is now open to unit testing. The bits that aren’t tested simply represent specific lines where we supply access to the MonoBehaviour-specific API. All the rest has been abstracted away.

The practical result for you – whether or not you care about Editor extensions or even unit testing – is safer, more reliable code in an area of the framework on which you probably rely a great deal.

The mea culpa and moral of all this: the single responsibility principle exists to help us. My failure to consider this fully when writing the original version of the MediationBinder is only coming clear to me years later. Had I broken down the elements of my methods into smaller atomic parts back then, we probably wouldn’t have written off the whole of this package as untestable.

Lesson learned.