When writing code to drive a user interface, there’s an unfortunate default tendency to scatter logic everywhere. At least, that’s what I do unless I apply conscious effort to avoid it. In this short post, I explain the problem and a simple solution.

Bad: Inline Logic

Suppose you need to disable a feature F when activities X or Y or Z are happening. The default way to solve this problem (for me when not thinking ahead) is to just find the code starting/stopping X/Y/Z and insert the necessary logic. Like this:

void OnClickedStartX() { ... FButton.Enabled = false; ... } void OnFinishX() { ... FButton.Enabled = true; ... } void OnStartY() { ... FButton.Enabled = false; SomeOtherButton.Enabled = false; ... } ...

Don’t do this. The end result is a bad place to be. Now, in order to determine if F is implemented correctly, unrelated architectural details like “Is OnClickedStartX the only way to start X?” and “Can Y be happening at the same time as Z?” must be understood. To make matters worse, those details are unlikely to be explicitly stated anywhere. In other words: the code is too highly coupled, making it brittle and unapproachable.

The “technique” exemplified above spreads single features across multiple methods and, conversely, spreads the code in single methods across multiple features. Verification and understanding can’t be done in small chunks, because everything ends up interconnected. Determining if a feature is implemented correctly requires determining the correctness of 10 methods, each of which involves code for 10 features, each of which … You get the idea. You will lose a lot of time if you take this approach.

Good: Triggered Logic

I decouple this sort of “F when X/Y/Z” code with events (or some other form of triggered callback). Instead of putting code-about-F inside code-about-X, you have an ‘OnXChanged’ event that code-about-X raises and code-about-F consumes. In this case, because triggered values are being combined and transformed, I think the clearest implementation uses reactive extensions (a.k.a. linq-to-events). The resulting code looks like this:

// the running states are exposed in a way that can be observed ObservableValue<bool> isXRunning = new ObservableValue<bool>(false); ObservableValue<bool> isYRunning = new ObservableValue<bool>(false); ObservableValue<bool> isZRunning = new ObservableValue<bool>(false); // the start/stop methods manage the relevant exposed states void OnClickedStartX() { ... isXRunning.SetValue(true); ... } void OnFinishX() { ... isXRunning.SetValue(false); ... } ... // F's logic is hooked up indirectly, via the observables void Setup() { ... // create an observable that is false when any of X, Y or Z are running IObservable<bool> shouldFBeEnabled = Observable.CombineLatest( isXRunning, isYRunning, isZRunning, (x, y, z) => !(x || y || z)) .DistinctUntilChanged(); // drive the button's enabled state with the observable's value shouldFBeEnabled.Subscribe(isEnabled => FButton.Enabled = isEnabled); ... }

This code is a bit longer, but much easier to understand and verify in pieces. Everything having to do with F is in one place. Everything having to do with X is in another place. Thus the code-about-F can be understood and verified independently of the code-about-X.

By using Rx, instead of raw events, we also get to take advantage of methods like CombineLatest and DistinctUntilChanged (why didn’t they call it WhenDifferent?). This makes life a lot easier, which is not too surprising since this sort of situation is exactly what Rx is designed for.

Summary

Decouple UI code with events and Rx. Not really an earth shattering idea (people have said it before). But, the benefit is significant enough that it bears repeating.

Next week: What if we don’t know what disables F? How can independent components, like plugins, affect F without trampling on each other’s toes?

—

Happy Holidays

—

—



Twisted Oak Studios offers consulting and development on high-tech interactive projects. Check out our portfolio, or Give us a shout if you have anything you think some really rad engineers should help you with.



Archive