Decoupling shared control

posted by Craig Gidney on January 1, 2013

Last week I ended on a question: how can multiple unknown components share control of something, without having to worry about trampling on each others’ toes? In this post I’ll talk about where the obvious approach falls short, and outline a solution my coworker calls “lenses”.

To introduce the problem: a tongue-in-cheek parable.

Parable

Setting: large conference room. Craig steps out onto the stage, to give his bold daring insights into the benefits of simplicity and just doing things the obvious way. He begins speaking.

“Hello, everyone. Welcome to ConferenceCon ++2012! My talk is about simple shared control.”

“Sometimes multiple parts of a program all need to affect a single part. The most straightforward way to do so is: make the thing-to-be-controlled publicly mutable. Want control? Just mutate the value. Easy!”

“Suppose you have a background video player object. Lots of things might want to pause video playback, so we’ll want a public ‘paused’ field. Doesn’t get simpler than that! Here’s some samples.”

Code samples swoosh onto the screen behind Craig, using the finest combination of scaling, rotating and shearing animation effects available.

// code for a play-pause toggle button
void OnTogglePlayPause() {
    videoPlayer.Paused = !videoPlayer.Paused;
}

// code to pause while a settings dialog is open
void OnOpenSettingsDialog() {
    videoPlayer.Paused = true;
}
void OnCloseSettingsDialog() {
    videoPlayer.Paused = false;
}

// code to pause while buffering data
void OnNeedMoreStreamedVideoData() {
    videoPlayer.Paused = true;
}
void OnHaveNeededVideoData() {
    videoPlayer.Paused = false;
}

“Of course, people will tell you that you should control public mutability but clearly— erm… wait, the settings dialog and buffering examples are wrong. My mistake. Opening and closing the settings shouldn’t unpause playback, of course! We’ll have to stash the previous state and restore it after…”

The faint sound of furious typing on a Model M keyboard is heard from backstage, until a new slide appears.

bool wasPausedBeforeOpeningSettings, wasPausedBeforeBuffering;
void OnOpenSettingsDialog() {
    wasPausedBeforeOpeningSettings = videoPlayer.Paused;
    videoPlayer.Paused = true;
}
void OnCloseSettingsDialog() {
    videoPlayer.Paused = wasPausedBeforeOpeningSettings;
}
void OnNeedMoreStreamedVideoData() {
    wasPausedBeforeButtering = videoPlayer.Paused;
    videoPlayer.Paused = true;
}
void OnHaveNeededVideoData() {
    videoPlayer.Paused = wasPausedBeforeBuffering;
}

“Anyways, like I was saying, raw direct access is the easiest way to— Yes?”

An audience member in the front row interrupts the presentation to ask a question. Craig begins answering.

“What happens if the user opens the settings during buffering? Well, wasPausedBeforeBuffering would be set to true and wasPausedBeforeOpeningSettings would be … …”

“Crap.”

“Ok, so we don’t actually want the video to start playing while the settings dialog is open, only for it to pause when dialog is closed. We can fix that by uh… hmmm…”

“No, that won’t work…”

“Maybe…”

“But that’s an abstraction (ugh!)…”

Craig stands limp, staring at the audience. The room is dead silent. Nothing left to do but calmly sprint off stage and never return.

End Scene

Bad: Direct Access

Alright, alright, I’ll stop. No one promised that adequate technical writing skills would imply any humor writing skills!

The underlying problem with the code in the parable is that, although whether or not a video player is paused can be described with a single boolean flag, the various reasons for it to be paused can’t. The video player code only cares about being paused or not, but the settings dialog code needs to affect user-paused differently from buffering-paused. Giving direct access to the internal state just isn’t enough to do shared control properly.

A possible quick and dirty fix is to split the single paused flag into multiple paused flags: Paused_User, Paused_NeedData, and Paused_ShowingDialog. Playback is paused when one or more of them is set. Why is this quick and dirty? First, it’s breaking encapsulation and scrambling your architecture. You shouldn’t be learning things about the front-end settings dialog when reading the back-end video player code. Second, it doesn’t solve the underlying problem. We want to work with unknown components that don’t exist yet, but you have to keep adding more fields whenever more reasons to be paused (e.g. lost focus) come along.

Actually solving the problem requires a slightly more general fix.

Good: Lenses

This type of shared control problem can be solved with “lenses”, where a lens is a temporary modification to how a value is viewed. For example, instead of directly setting “Paused” to true, you push an “appear true” lens over it. To unpause, you remove the lens you added. Whenever there’s one or more “appear true” lenses present, playback is paused. Similarly, to allow shared control of a damage statistic, you would add/remove lenses with effects like “plus 10″ and “times 2″ (but note that ordering matters in that case).

Lets actually implement this for the ‘pausing video playback’ scenario. In a more general situation we would need to track a list of lenses and apply them each whenever someone queried the value, but “appear true” is so simple we can just keep a count (because it’s idempotent):

public class VideoPlayer {
    private int _pauseCount;
    public bool IsPaused { get { return _pauseCount > 0; } }
    
    public void AddPause() {
        _pauseCount += 1;
    }
    public void RemovePause() {
        if (_pauseCount <= 0) throw new InvalidOperationException("uh oh...");
        _pauseCount -= 1;
    }
}

Now anything anywhere can pause playback temporarily, without having to worry about later restoring previous states that couldn't be inspected to begin with. As long as each individual component handles pauses correctly within itself, everything will work fine. Those components look like this:

// code for a play-pause toggle button
bool isManuallyPaused = false;
void OnTogglePlayPause() {
    isManuallyPaused = !isManuallyPaused;
    if (isManuallyPaused) {
        videoPlayer.AddPause();
    } else {
        videoPlayer.RemovePause();
    }
}

// code to pause while a settings dialog is open
void OnStartSeek() {
    videoPlayer.AddPause();
}
void OnEndSeek() {
    videoPlayer.RemovePause();
}

// code to pause while buffering data
void OnNeedMoreStreamedVideoData() {
    videoPlayer.AddPause();
}
void OnHaveNeededVideoData() {
    videoPlayer.RemovePause();
}

We're forced to have a boolean representing the user's desired playback state, but we can locate it where it belongs (in the UI code).

Notice that more features that involve pausing can be added later, without changing or even thinking about the settings dialog code. The scope of verifying "is it pausing correctly?" has been reduced, so that we can do it component by component, instead of having to consider everything that touches the field at once. The runtime state still depends on all of the components, but they can be analyzed independently. We've decoupled the shared control over pausing.

I don't mean to imply that every instance of shared control ever can be solved with lenses. That's not the case. But there's plenty of simple ones, like being paused or setting maximum volumes, that it applies to.

Summary

Giving direct access to internal state is not good enough to enable proper shared control. It forces anything that touches the state to consider everything else that touches the state, lest they interfere with each other. An alternative approach is to expose an API for stacking lenses (temporary modifications) over the state.

Using lenses reduces coupling, but does have the downside of having to match Add calls with Remove calls. This can be helped a bit by using CancellationTokens, but I'll be exploring that idea next week.

---

Discuss on Reddit, Hacker News

---


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

More interesting posts (7 of 8 articles)

Or check out our Portfolio.