In a previous post, we talked about untangling multiple UI controls so that they could be developed independently, but react to user interaction in a synchronized manner. Let’s posit for a moment updates to a line on one map control should cause re-rendering of a cross-section control associated with that line, but that the two controls are in different browsers, or even on different machines.
All the cool kids will tell you that a service bus will give you the decoupling you need across processes. Perhaps we should just use this!
Pretty simple, and fairly decoupled. Well, the controls are decoupled from each other as we outlined before, but the controller is now tightly coupled to the service bus abstraction.
Most programmers at least stop and pause a bit when they see a global variable. But the love for shiny service buses can override that sensible caution. How can we reconcile the fact that a service bus does provide us with the useful abstraction that allows decoupling of controls running in different process, and yet as a global state bucket, it’s something to be feared?
In this case, I would argue that the controls and controller should be bound only to the abstractions they need — IObservable<Polyline> — and not the ones they happen to use — GlobalServiceBus. In this case, the MapControl and the CrossSectionControl expose the Polyline property using just the abstraction:
If we ignore some fine-grained details (like throttling updates that are duplicates or happen too quickly) this is pretty straightforward. The cross-section control will now update itself when the map changes. The behavior and the controllers are hooked up cleanly, using all the abstractions they need to use. Note that the controller didn’t even take a dependency on the controls themselves, just the abstraction from the controls it needed.
What happened to the service bus? That’s an implementation detail of the infrastructure. If the map and cross-section controls are in the same process space, then you don’t need a service bus, you just need the function calls used to implement IObservable, and your work is done.
If you do work cross-process, then you just write a small piece of code — identified explicitly as infrastructure setup — that creates the coupling.
The controls don’t know about each other, the behavior controller doesn’t know about the service bus, they can all be tested independently. The one person who “wires everything up” on startup has to know, of course, about where infrastructure components are, and can do so in a completely isolated way. Message bus formats can change, implementations can be swapped, security can be added, etc. all without affecting the basic behavior of the controls and behavior controller.
This exercise isn’t meant to show that a service bus is bad — on the contrary it’s quite a useful piece of kit. But it’s too big and too flexible a global variable to go taking dependencies on willy-nilly. Think about what abstraction from it you really need and use it instead, and isolate it to an infrastructure setup function.
Isn’t it nice to test the behaviors without an actual service bus installed? And once you’ve got it to those one liners, is there much to test?
Another common anti-pattern is to post a message to a message bus to call a remote function, and then listen to the bus for confirmation it’s been completed. This might effectively represent a remote function call – Task<T>. Or starting a job and looking for progress messages — back to our friend IObservable<ProgressMessage>. Or a fire-and-forget log message — Action<Message>. Give it a try and see if you can’t get all your service-bus code into one file that runs on startup, converting infrastructure-specific gotchas into clean application abstractions. Then all your tricks can be in one place, where you can corner them and beat them into submission.