Ahoy, trade-offs ahead.
When surveying UI frameworks and libraries, the vast majority of them use handler registrations. You know, that thing where you say
Register(actionOrMessage, MyHandler) or something like that. Since we started using MVU several years back, it introduced a new (but very old) way of matching data to a handler. By literally switch/case/match-ing the data, and calling a function specific to that data case. I'm calling that "function wiring". It looks like this.
let update msg model = match msg with | LogOutClicked -> model, [LogOut] | Home msg -> Home.update msg model | Search msg -> Search.update msg model ...
This was like a breath of fresh air. Things were simple and traceable. No more opaque machinery leaving you wondering how exactly your handler gets called. You can find and touch the code that calls it. You want to implement an "interceptor" aka "decorator pattern"? No problem! Just wrap the call to the function in another function. No complicated machinery, just functions. You can even do it for all handlers at once in a pretty obvious way.
let logged f msg inmodel = let model, effects = f msg inmodel model, effects @ [LogUpdate (msg, model, effects)] // update msg model logged update msg model
Function wiring has amazing benefits versus handler registration. But they don't come for free.
Handler registration lets you organize the pieces of your application however you like because they are not directly connected to one another unless they need to be so. Function wiring enforces a structure on how your code is connected, typically a hierarchy. The problem with a hierarchy is that it is another thing that has to be maintained.
Code wise, the hierarchy is not really any more code than registration handlers. I could have turned those match cases into one liners. A line or two in this file versus that one. So the code isn't any worse to maintain. It is the concept of the hierarchy itself that adds maintenance. The dev has to think about how to fit into the function wiring hierarchy when they change things. This is on top of file organization, and it is helpful to find a way to align these two concerns.
Git merge conflicts are common with function wiring when two developers are working in sibling areas in the hierarchy. The developers both work on their features on their own branches. They are likely to wire up functions in the same place -- at the end of the match list. Resolving the conflict is easy (take both side) but it is still annoying.
This isn't all bad, as it gives your work context. You are forced to think about how it fits in. But it also adds inertia.
I've been pretty down on registration handlers these last few years. I have observed that they tend to go along with frameworky and over-abstracted code. But does it have to be that way? Is there some middle ground?
I am starting to come back around to the idea that maybe there is a way to use them well. Consider namespaces. In some languages (example: Clojure) these are side effecting registration handlers. But they still work and stay out of the way. Why? Because they only do one thing -- give names to collections of things. Maybe single-responsibility registrations could be the right answer for some situations and function wiring for others.
Per usual, trade-offs of each strategy have to be matched against the problem.