fbpx

Blogs from the Ranch

< Back to Our Blog

Event buses, NSNotificationCenter and @channels

Avatar

Bill Phillips

This is a post about mobile app development. Before I get to that, though, I have a little beef I need to talk about.

Big Nerd Ranch, like many other companies, uses Slack. Slack is a well-done glorified chat app where you join or create channels and chat with people, or you can message folks directly if you so choose.

One of the things I like the most about Slack is its well-thought-out notification system. Normal channel messages are highlighted when I switch to the app or open it up, so that I can catch up on what I haven’t read. If someone messages me directly, though, or calls me out by name in one of the channels I’m lurking in by saying @bphi, I get a little sound and a pop up notification on my computer. If I’m not at my computer at that moment, I’ll even get an e-mail.

You can tweak all these things, but that’s the basic idea. Notifications interrupt me, while everything else doesn’t.

The Infamous @channel

This brings us to the @channel. When a user types @channel, Slack sends a notification to every single person in a particular room, waking them up to go read what you have to say.

As you can imagine, @channels are controversial. Some folks rely on them heavily, because they rarely interact with Slack at all. They are focused on their work, and don’t keep a chat app open. These folks don’t relish the idea of opening up Slack, asking their question and then waiting for a response. Using an @channel allows them to open Slack, ask a question and get an immediate response. It just works.

Not necessarily for other folks, though. The many folks who were briefly interrupted probably will not say anything in response to an offhanded, unnecessary @channel. They will grumble briefly to themselves and return back to their work. Some people may curse aloud at their desk, or run off and write a blog post (cough). More likely than not, these folks will just turn off @channel notifications entirely.

Should you turn them off, though? Is that a good idea?

Technical Problems and Social Problems

I think that we underestimate the degree to which technical problems correspond to social and cultural problems. Sure, we talk about Conway’s law in our tongue-in-cheek kind of way, but rarely do we actually talk about how we might better change our code by changing how we interact with one another.

I think we can even consider our own programs to be like little snowglobe societies. (I don’t think I’m the only one, either.) The single responsibility principle? It’s the programmer’s version of the golden rule. And if we’re inconsistent people, we’re going to make inconsistent little reflections of ourselves. And the same thing applies to how our code fits in with our peers’ code—if we’re not sure how we fit into our team, our code won’t know how it fits in, either.

This problem with @channels does look a bit like a technical problem—it’s a mechanism that just isn’t useful. Anyone can abuse it and interrupt everybody, and they probably will. So get rid of it.

But it’s partly a social problem, too. Not every organization will respond in the same way to the presence of the @channel. Some organizations will be filled with people using them for everything, everyone interrupting one another; in others, they will serve exactly their intended purpose, only being used to wake up listeners for an important event. Perhaps some organizations are better served by interrupting everyone. I doubt it, but I don’t know everything.

Event Buses

So what does this have to do with mobile app development? Recall that I was supposed to be talking about event buses and NSNotificationCenter. These are tools for two different platforms, each of which works in a slightly different way. They usually fulfill similar roles on your app’s architecture, though.

I’m an Android guy, so let’s talk about the event bus first. Here’s what an event bus is: an object that you can subscribe to events on. So you might write the following code in a Fragment:

public class WaiterFragment extends Fragment {
    ...

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mEventBus.subscribe(this);
    }

    public void onEvent(FireAlarmEvent event) {
        fleeInTerror();
    }

    ...
}

And then off in your FireDetector, you might fire the event.

public class FireDetector {
    private EventBus mEventBus;

    public FireDetector(EventBus eventBus) {
        mEventBus = eventBus;
    }

    ...

    public fireDetected() {
        mEventBus.send(new FireAlarmEvent());
    }
}

This will call onEvent(FireAlarmEvent) for everyone who is subscribed to that particular event.

On iOS, it’s a similar story. Events are identified by well-known string constants and propagated through an NSNotificationCenter.

class Villager {
    func fleeInTerror() {
        /* flee */
    }

    init() {
        prepareToFlee()
    }

    deinit {
        removeAllObservers()
    }

    func prepareToFlee() {
        let notificationCenter = NSNotificationCenter.defaultCenter()
        let observer = notificationCenter.addObserverForName(
            FireAlarmDidSound, object: nil,
            queue: NSOperationQueue.mainQueue()) {
                [weak self]
                _ in self?.fleeInTerror()
        }

        observers.append(observer)
    }

    private var observers = [AnyObject]()
    private func removeAllObservers() {
        let notificationCenter = NSNotificationCenter.defaultCenter()
        for o in observers { notificationCenter.removeObserver(o) }
    }
}

You can post the event like so:

let FireAlarmDidSound = "FireAlarmDidSound"

class FireDetector {
    func didDetectFire() {
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.postNotificationName(
            FireAlarmDidSound, object: nil)
    }
}

Android has a communication problem, to put it mildly. In lecture, I have been known to describe Android as “a place where any one of your friends might die or come back to life at any time.” It is a framework where the operating system is in charge of a lot of important lifecycles, and you are not.

As a result, it can be a hassle to hook up point-to-point communication between different components in the system. A lot of this is a good thing—communicating directly from one Activity to another is a bad idea, whether it is difficult or not.

It’s not just Android, though. Any client app usually ends up having this problem to some degree. Screen controllers tend to either spawn each other in long chains, or be spawned by an overlord of some kind. When several of them need to be informed of the same event, you need to wire an observer.

As these observers proliferate, your controllers get cluttered with code finding the place a specific event is coming up, wiring up a listener to it and making sure to unsubscribe when you’re done. An event bus or notification center solves that problem.

Why Event Buses Can Be Horrible

So there are good reasons people like event buses. Why have people come to hate them? In short, event buses can be abused to create spaghetti code.

Say that you have written an restaurant simulator app that features an event bus. Your app has a HostessStandFragment, where your user walks up to the hostess stand and tries to get a table. In your next new feature, HostessStandFragment gets a new button that allows diners to place to-go orders.

The hostess doesn’t make the food, of course. The kitchen makes the food, and the patrons order it from waiters. Except now the hostesses are ordering food, too.

The event bus provides an immediate solution to this problem: all you have to do is create an event class, subscribe to it in the recipient and send it in the sender—bam, you’re connected. If you need to pass anything along, just include it in your event object. On iOS, you would include a payload of some kind.

In the case I showed here, that is all well and good. In other cases, it can cause problems. If you have an event bus, you can connect any point A to any point B, as long as both points have a bus handy. Two views can easily update one another, if both have a bus handy.

The problem is surprisingly similar to the one caused by gotos: one could always build well-designed programs off of gotos, but one developer with a burning personal need could always jump through the walls you lovingly constructed with a couple of lines of code. With the goto, this misuse broke your ability to think about the line-by-line sequencing of your code. With the event bus, such misuses break your ability to think about the lattice of responsibilities your classes represent.

The negative results are much different than they are with @channels. Instead of interrupting everyone, control flow becomes confused, jumping randomly from class to class to follow the immediate needs of your code. Instead of being localized to one area of your app, you end up having to follow the thread all over the place to figure out how everything works. And when one event fires another even? Ooh, man.

A Social View of Events

The @channel problem made me think about this in another way, though. As one person, there’s only so much I can do about @channels. That limitation shapes my thinking on how I can really make things better for the people around me, not just myself.

As an engineer, I don’t like it when something is a social problem, because I can’t impose my own solution. Even if I’m the boss, I don’t get to choose what other people do. I can’t make people believe in me if they don’t. They’re people just like I am, and they do whatever the hell they want to. I can rant about @channels all day, but it won’t do a lick of good for anybody if my colleague still finds the @channel useful and either isn’t concerned about, or isn’t aware of the negative impact it might have on others.

Social problems aren’t amenable to quick fixes. I have to trust my colleagues to fix my problem for me, as if my problem were theirs. That trust is powerful, but it takes time and patience to build it up. And you might break it down again if you go on too many angry rants.

So when I’m dealing with my colleagues, the rule of thumb is simple: appreciate their humanity, and that we are all doing our best to do what we believe is good. For a small thing like @channels, kindly making my colleagues aware of the effect their actions have on me is probably all I should do, unless I’m in a position of some influence.

If you think of your objects from a social perspective, posting an event to accomplish something your object cares about is a severe boundary violation. It may be convenient, but it ends up being corrosive to the cohesiveness of your design because your object is divesting itself of its responsibilities in a thoughtless manner.

Typical Solutions

This is not an abstract issue. I’ve run into event bus spaghetti code in my own work, and it’s not pretty. My iOS colleagues have the same issues with NSNotificationCenter.

So how can this spaghetti code be avoided? I’ve seen or been tempted by two tactics:

  1. Just don’t use an event bus at all. This is the goto solution: it does bad things, it’s bad, get rid of it. The assumption here is that anything you can do with an event bus can be better accomplished with some other construct, even if that means writing a lot more code.

  2. Enforce some kind of narrow, codified design restriction on how the bus is used. For example, “Only post events from long-lived components in the model layer, and only subscribe to them from within the controller layer.” The rule would be clear and unambiguous. The hope is that this establishes clear lines of responsibility, and eliminates the horrible nested event triggering problem.

I think solution #1 is valid, but I don’t like it. I like having this tool in my toolbox. Solution #2 makes sense, but I think there’s a deeper idea at work.

A Rule of Thumb

So: when should you post an event? When should you send an @channel?

Here’s my proposed rule for both situations: You should break into someone else’s context only if it is your legitimate responsibility to them to do so.

Every time you define an event or notification, every time you use an @channel, you pass the buck to someone else. With a normal point-to-point observation, you are performing either a hierarchical or peer handoff of responsibility.

With a broadcast observation, however, you perform a communal handoff of responsibility: like a fire alarm, you pass the buck to someone, probably multiple someones, and you do it in a way that is by design open to all. Importantly, your counterparty is not expecting anything from you specifically.

This means two things: it matters what your responsibility is, and it matters what their responsibility is. You need to have a responsibility to let a crowd of receivers know; they need to have a responsibility to take some action on that event. Without both of those in play, you will want to use a different tool.

Bad Uses

In Android, the most common misuse is in the case where creating a more appropriate hookup is just a hassle.

Here’s an example. Big Nerd Ranch has an app that handles conference room bookings. New bookings are created by a Service object behind the scenes. To inform the front end that a new booking is available, an event is fired.

Does everyone have a responsibility to act on a room booking? Well… no, not really. Only specific parts of the front end do. The main reason this event exists is that the right solution is to create a separate component to send the event through, and creating a separate component is a pain.

The shortcuts can get even worse than that. The same app has an event named DismissInfoDialogEvent. This event is fired to signify that a DialogFragment was dismissed. In all likelihood, the only other object responsible for acting on that lifecycle event is the object that showed it in the first place. This is a bad use.

The @channel operates in a far more concrete space than the event or the notification, so this should be more clear: your own responsibilities might require you to send an @channel, but respect those of others as well. If you’re taking someone else’s time, be respectful of that time.

Good Uses

The FireAlarmEvent example we used earlier is a great example of this. A fire alarm going off changes how everyone in the building should behave: they should head for the exits. Specific people may have additional responsibilities, like grabbing their pets and children. So you have a responsibility yourself (the discovery), to them to let them know.

What about a real example? Well, in apps we usually don’t have fire alarms, but we do have to trigger and display errors. That fits the model: “I have a responsibility to some other component to tell them this happened. They may not know who I am, though, and I will not know who they are.”

We have minor emergencies that are probably appropriate for @channels, like an overflowing toilet. Some events are appropriate for @channels, too, like when the all-hands company meeting is starting. Some may use an @channel if they are locked out of the building. For each one of these, the rule applies: the recipient would want to know, and the sender is the responsible party to tell them, even though they do not know each other specifically.

In Conclusion: Hmm

If you take a hard look at usage of this rule, you will see that while the event or notification is a tool that you can use to do almost anything, its proper uses are far more rare. Good uses for events or notifications are difficult to find, because few events in an application have such a broad scope, or can be fired from so many places.

When I discussed notifications and events with my colleagues, the pain point that came up was what they called “nested” events. This is when one event ends up triggering another event, which triggers another event, and so on.

This might be the pain point, but it’s just not a problem that’s unique to events. All kinds of observers and listeners suffer from this same problem. If all you do is switch to using some other mechanism besides an event clearing house, you’ll still have the same problem.

That’s probably the biggest lesson I found here: there just aren’t any easy answers to this stuff. Unlike with the goto, getting rid of observers isn’t a realistic solution.

I like simple guiding rules for things. In practice, things are always going to get hairy. A simple rule is always going to be hard to apply, but it can be a guiding point on the horizon.

Avatar

Bill Phillips

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project