[SDKs & UI Libraries] The MV* Frameworks
With this post, I’m continuing publishing the v2 of my book dedicated to APIs. If you like this book, please rate it on GitHub, Amazon, or Goodreads and
One obvious approach to reducing the complexity of implementing the multi-layered component hierarchies we described in the previous chapter is to restrict possible interaction directions. As we described in the “Weak Coupling” chapter, we could simplify the implementation if we allow subcomponents to call the parent context's methods directly:
class SearchBoxComposer
implements ISearchBoxComposer {
…
protected context: ISearchBox;
…
public createOrder(offerId: string) {
const offer = this.findOfferById(offerId);
if (offer !== null) {
// Instead of generating an event
// this.events.emit(
// 'createOrder', { offer });
this.context
.createOrder(offer);
}
}
}
Additionally, we may relieve Composer
of data preparation duty and allow subcomponents to retrieve the data fields they need from SearchBox
directly:
class OfferListComponent
implements IOfferListComponent {
…
protected context: SearchBox;
…
constructor () {
…
// The offer list component
// takes data from `SearchBox`
// and listens to state changes
this.context.events.on(
'offerListChange',
() => {
this.show(
this.context.getOfferList()
);
}
);
}
…
}
As we lose the ability to prepare data for subcomponents, we can no longer attach subcomponents to different parents through implementing a custom Composer
. However, we can still replace them with alternative implementations, as the reactions to user's actions are still controlled by Composer
. As a bonus, we now don't have two-way interactions between our entities:
Subcomponents read
SearchBox
's state but never modify it.Composer
gets notified about the user's interaction with the UI but doesn't interfereFinally,
SearchBox
doesn't interact with either of them and only provides a context, methods to change it, and the corresponding notifications.
By making these reductions, in fact, we end up with a setup that follows the “Model-View-Controller” (MVC) methodology1. OfferList
and OfferPanel
(also, the code that displays the input field) constitute a view that the user observes and interacts with. Composer
is a controller that listens to the view's events and modifies a model (SearchBox
itself).
NB: to follow the letter of the paradigm, we must separate the model, which will be responsible only for the data, from SearchBox
itself. We leave this exercise to the reader.
MVC entities interaction chart
If we choose other options for reducing interaction directions, we will get other MV* frameworks (such as Model-View-Viewmodel, Model-View-Presenter, etc.). All of them are ultimately based on the “Model” pattern.
The “Model” Pattern
The common denominator of all MV* frameworks is the requirement for the “model” entity to fully deterministically define the look and state of a UI component. Changes in a model beget changes in views (or the hierarchy of views as in some approaches a model could be global and define the look of the entire application). Meanwhile, visual components cannot affect the model directly as they only interact with controllers.
SDKs that implement one of the MV* paradigms theoretically gain important advantages:
Mandatory separation of data domains as it is prescribed (though not necessarily followed, see below) that a model contains sematic high-level data.
The event loop cycles are almost impossible since controllers should only react to the user's or developer's interaction with views, not model changes.
Additionally, model state change events are usually generated if and only if the state really changed (i.e., the new field value differs from the current one). To make a loop, the system needs to infinitely oscillate between two distinct states which is rather unlikely to happen accidentally.
Controllers translate low-level events (user's actions in the UI) into high-level ones thus providing sufficient abstraction to allow changing the underlying UI while preserving business logic.
As the model data fully defines the system state, it is very convenient for implementing such complex functionality as restoring after a crash, collaborative editing, undoing the last changes, etc.
One of the use cases to utilize this property is serializing a model in the form of a URL (or an App Link in the case of mobile applications). Then the URL fully defines the application state, and all state changes are reflected as URL changes. This comes in handy as it allows generating links that open any specific screen in the application.
In conclusion, MV* frameworks establish a rigid pattern that helps in writing quality code and effectively controlling data flows.
This rigidity, however, bears disadvantages as well. If we try to fully define the component's state, we must include such technicalities as, let's say, all animations being executed (and even the current percentages of execution). Therefore, a model will include all data of all abstraction levels for both hierarchies (semantic and visual) and also the calculated option values. In our example, this means that the model will store, for example, the currentSelectedOffer
field for OfferPanel
to use, the list of buttons in the panel, and even the calculated icon URLs for those buttons.
Such a full model poses a problem not only semantically and theoretically (as it mixes up heterogeneous data in one entity) but also very practically. Serializing such models will be bound to a specific API or application version (as they store all the technical fields, including those not exposed publicly in the API). Changing subcomponent implementation will result in breaking backward compatibility as old links and cached state will be unrestorable (or we will have to maintain a compatibility level to interpret serialized models from past versions).
Another ideological problem is organizing nested controllers. If there are subordinate subcomponents in the system, all the problems that an MV* approach solved return at a higher level: we have to allow nested controllers either to modify a global model or to call parent controllers. Both solutions imply strong coupling and require exquisite interface design skill; otherwise reusing components will be very hard.
If we take a closer look at modern UI libraries that claim to employ MV* paradigms, we will learn they employ it quite loosely. Usually, only the main principle that a model defines UI and can only be modified through controllers is adopted. Nested components usually have their own models (in most cases, comprising a subset of the parent model enriched with the component's own state), and the global model contains only a limited number of fields. This approach is implemented in many modern UI frameworks, including those that claim they have nothing to do with MV* paradigms (React, for instance2 3).
All these problems of the MVC paradigm were highlighted by Martin Fowler in his “GUI Architectures” essay.4 The proposed solution is the “Model-View-Presenter” framework, in which the controller entity is replaced with a presenter. The responsibility of the presenter is not only translating events, but preparing data for views as well. This allows for full separation of abstraction levels (a model now stores only semantic data while a presenter transforms it into low-level parameters that define UI look; the set of these parameters is called the “Application Model” or “Presentation Model” in Fowler's text).
MVP entities interaction chart
Fowler's paradigm closely resembles the Composer
concept we discussed in the previous chapter with one notable deviation. In MVP, a presenter is stateless (with possible exceptions of caches and closures) and it only deduces the data needed by views from the model data. If some low-level property needs to be manipulated, such as text color, the model needs to be extended in a manner that allows the presenter to calculate text color based on some high-level model data field. This concept significantly narrows the capability to replace subcomponents with alternate implementations.
NB: let us clarify that the author of this book is not proposing Composer
as an alternative MV* methodology. The message in the previous chapter is that complex scenarios of decomposing UI components are only solved with artificially-introduced “bridges” of additional abstraction layers. How this bridge is called and what rules it brings are not as important.
References
[1] MVC
https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
[2] Why did we build React?
https://legacy.reactjs.org/blog/2013/06/05/why-react.html
[3] Mattiazzi, R. How React and Redux brought back MVC and everyone loved it
https://rangle.io/blog/how-react-and-redux-brought-back-mvc-and-everyone-loved-it
[4] Fowler, M. GUI Architectures
https://www.martinfowler.com/eaaDev/uiArchs.html
This is Chapter 45 of “The API” book being written by Sergey Konstantinov. I also have a book on the history of beer and historical beer styles, a Telegram channel on interesting classical music recordings, a travel photo blog on Unsplash, and a website with ranking fantasy & science fiction novels based on awards they received.