This is actually a very good question - and probably one without a definitive answer. It's a problem that applies to many frameworks, not just GWT, so I like your idea to look at this with some simplified code. I'll make this a little bit longer, to show what even just 4 very simple callbacks look like:
Nested callbacks
alice.call("a", new Callback() {
@Override
public void onSuccess() {
bob.call("b", new Callback() {
@Override
public void onSuccess() {
charlie.call("c", new Callback() {
@Override
public void onSuccess() {
daisy.call("d", new Callback() {
@Override
public void onSuccess() {
// finished
}
});
}
});
}
});
}
});
Named callbacks
You can use your IDE to refactor this easily into named callbacks (hint: Please read the callbacks from bottom to top!):
final Callback daisyCallback = new Callback() {
@Override
public void onSuccess() {
// finished
}
};
final Callback charlieCallback = new Callback() {
@Override
public void onSuccess() {
daisy.call("d", daisyCallback);
}
};
final Callback bobCallback = new Callback() {
@Override
public void onSuccess() {
charlie.call("c", charlieCallback);
}
};
final Callback aliceCallback = new Callback() {
@Override
public void onSuccess() {
bob.call("b", bobCallback);
}
};
alice.call("a", aliceCallback);
- Problem: The control flow is not so immediately obvious anymore.
- Still, an IDE can help by using "Search References" (Ctrl-G in Eclipse) or something similar.
Event Bus (or Observer/Publish-Subscribe pattern)
This is how the same calls look like with an event bus:
alice.call("a", new Callback() {
@Override
public void onSuccess() {
bus.fireEvent(BusEvent.ALICE_SUCCESSFUL_EVENT);
}
});
bus.addEventListener(BusEvent.ALICE_SUCCESSFUL_EVENT, new BusEventListener() {
@Override
public void onEvent(final BusEvent busEvent) {
bob.call("b", new Callback() {
@Override
public void onSuccess() {
bus.fireEvent(BusEvent.BOB_SUCCESSFUL_EVENT);
}
});
}
});
bus.addEventListener(BusEvent.BOB_SUCCESSFUL_EVENT, new BusEventListener() {
@Override
public void onEvent(final BusEvent busEvent) {
charlie.call("c", new Callback() {
@Override
public void onSuccess() {
bus.fireEvent(BusEvent.CHARLIE_SUCCESSFUL_EVENT);
}
});
}
});
bus.addEventListener(BusEvent.CHARLIE_SUCCESSFUL_EVENT, new BusEventListener() {
@Override
public void onEvent(final BusEvent busEvent) {
daisy.call("d", new Callback() {
@Override
public void onSuccess() {
bus.fireEvent(BusEvent.DAISY_SUCCESSFUL_EVENT);
}
});
}
});
bus.addEventListener(BusEvent.DAISY_SUCCESSFUL_EVENT, new BusEventListener() {
@Override
public void onEvent(final BusEvent busEvent) {
// finished
}
});
- Under the right circumstances (when it's very clear what each event means, and
if you don't have too many), this pattern can make things very nice and clear.
- But in other cases, it can make the control flow more confusing (and you easily get twice the lines of code).
- It's harder to use your IDE to find out about the control flow.
- The GWT History mechanism is a very positive example for where to use this technique reasonably.
Divide and Conquer
In my experience, it's often a good idea to "divide and conquer" by mixing nesting and named callbacks:
final Callback charlieCallback = new Callback() {
@Override
public void onSuccess() {
daisy.call("d", new Callback() {
@Override
public void onSuccess() {
// finished
}
});
}
};
alice.call("a", new Callback() {
@Override
public void onSuccess() {
bob.call("b", new Callback() {
@Override
public void onSuccess() {
charlie.call("c", charlieCallback);
}
});
}
});
Depending on the situation, two nested callbacks are often still readable, and they reduce jumping around between methods when reading the code by 50%.
(I created a pastebin of my examples here, if you like to play around with them: http://pastebin.com/yNc9Cqtb)