Note: this article assumes that you have a basic understanding of the Reactive Extensions concepts and usage. Also, several references to the iOS application architecture are made along with the story. However, these concepts are not critical for understanding the main ideas of the article.
Developing GUI application architecture is hard. The structures that control the behavior and appearance of our views may quickly get overcomplicated and overengineered. However, there are certain tools that are helpful in keeping the code simple and maintainable. In Makimo we decided to try out mixing several ideas to design clean application architecture. From a combination of Reactive Extensions, MVVM and Coordinator Pattern we created the pattern that, for the purpose of this article, is called Reactive Coordinators. Although the Coordinator Pattern was bred for and by the iOS developers, we will present proof of concept in Node.js. The PoC will be a simple terminal based application that handles user authentication against some API.
The Coordinator Pattern
The Coordinator Pattern was introduced in 2015 by Soroush Khanlou. It solves the problem of placing the navigation logic within the iOS application. Previously, the code controlling transitions between the views was placed within UIViewControllers
which were also responsible for providing data for the views, handling user input, contacting data sources and so on. This usually led to Massive View Controller antipattern. A lack of separation of concerns is implied in the official Apple developer documentation, but this is a topic for another time. Apart from being overstuffed, the UIViewControllers
needed to know about each other to manage the transitions: for instance, the login view needed to know where the user should be after successful login. As a result, tight coupling between views that logically were not as close to each other was plaguing iOS applications.
Soroush Khanlou suggested extraction of navigation logic into the separate components that would oversee the transitions between views. These components, called coordinators, should be responsible for creating the views, injecting dependencies and presenting them to the user. The coordinator reference must be passed to the managed view to allow backward communication: the view may notify that it was dismissed or completed so the coordinator can show another view (for instance present dashboard after successful login).
The coordinators themselves are organized into a tree structure. This allows to separate application parts: for example, the authentication flow can be controlled by different coordinator than the product purchase flow in a shopping app. The parent coordinator passes the control to its child and regains it when the child decides that it is done.
The Coordinator Pattern allows the UIViewControllers
to be loosely coupled, thus increasing their testability and maintainability. The programmer can organize the flow of an app with ease and flexibility.
Model-View-ViewModel
Another way of decluttering the UIViewController
was the integration of Model-View-View Model architecture. The MVVM was announced by Microsoft in 2005. This pattern separates the view layer from a model with a component called a View Model.
The main responsibility of the View Model is the preparation of data provided by the Model layer (for example from database or API) so it is ready to be displayed by the View layer. The Views in this architecture should be stripped of any logic as much as possible. All formatting and data validation must be done by the View Model. Furthermore, the View Model is responsible for handling user input and modifying the Model accordingly. For example, if a user submits an order form, the View Model contacts Model layer and commits the purchase. At the same time, View Model notifies the View that some sort of progress bar or activity indicator should be displayed.
Communication between View Model and View is achieved by data bindings. They may be unidirectional (for example displaying product details on the product page) or bidirectional (handling order form during the purchase process). Data bindings should carry display-ready data so that they need no additional processing on the View side.
This leads the much cleaner architecture with a clear separation of concerns. Also, the testability is greatly improved as the View Models can be tested separately from the Views.
Reactive Extensions
MVVM plays very nice with the Reactive Extensions. In MVVM the reactive streams are usually used to do the data binding between the Views and View Models. The View Model provides observables that are subscribed by View and which stream data that should be displayed. The other way round, the View provides observables that represent user actions, ie. button clicks. After the bindings are done, the View can react to the changing data and View Model can dispatch actions in response to user actions.
The complete idea
To sum up, our proposed application architecture consists of the following components:
- Model which manages business logic and performs data access.
- View which displays data provided by the View Model and notifies the Coordinator about need for navigation with observables.
- View Model which communicates with the Model and orders it to perform certain operations on the application data in response to user events obtained from View and passes the data that should be displayed in response; all the communication is done via Rx observables.
- Coordinator which navigates between Views and injects dependencies both into Views and Views Models.
The secret sauce
There was one more thing we added to the mixture to create the Reactive Coordinators. Usually, the Coordinators and View Models are modeled as instances of some classes providing required functionalities. Data bindings between View Model and View are modeled as properties of these objects, while the user input is handled via methods.
However, there is no need for them to be objects at all. The Coordinators, View Models and even Views may be modeled as pure functions, accepting several observables as inputs and returning other observables as outputs. The purity of these entities forces them to be stateless. As a result, the programmer can concentrate on data flows that occur within a developed application instead of managing the state.
In our proof of concept, we decided to ditch the Views, View Models and Coordinator modeled as objects and make them pure functions. As you will see in a moment, we got a demo that has virtually no state management on the programmer side of things.
The demo
To illustrate the concept of Reactive Coordinators we have created a simple demo application (actually, part of it). The demo is a simple terminal UI application that performs authentication of a user against remote API. The user may be authenticated either by passing credentials (e-mail and password) or by registering a new account. The demo was written in JavaScript for Node.js environment using Blessed.js library, which is pure JS implementation of ncurses, enriched with several widgets that made the prototyping fast enough. Although Blessed seems to be unmaintained and we do not recommend using it in production code, it was good enough for the proof of concept. As a reactive streams library, we have used RxJS in the current stable version.
Entrypoint
After running the main entry point, the first coordinator should be started. As was said previously, the coordinators are organized in a tree. The root of the tree is also a coordinator which is usually called an App Coordinator. In our proof of concept, it is modeled as a pure function appCoordinator
:
The screen
is a Blessed.js object that is used to render widgets in the terminal. It is used to hold the hierarchy of the displayed views. The first thing to notice with our concept: the dependency injection is implemented with passing proper function arguments. Since the navigation flow is controlled by the programmer, there is no need to use any external libraries that manage the dependency injection logic.
The dispose
is an observable that emits value when the appCoordinator
is finished (we’ll get to this in a moment). The connector
is a group of functions that implement API communication. In this case, these functions are register
and login
:
Application coordinator
The application coordinator has a simple task. It should invoke its child, the authCoordinator
which is responsible for handling authentication flow (either registration or login) and wait for its result:
Auth coordinator
The authentication coordinator is a bit more interesting. Initially, it presents the screen with two buttons which allow a user to display either login or registration form. The user may choose whichever option to perform authentication. The authentication coordinator should notify its parent when the API returned valid token as a result of either operation.
The showLogin
and showRegister
are functions where our coordinators start interacting with a view:
Both showLogin
and showRegister
call functions that set up corresponding views (loginView
and registerView
). These functions return observables that are used as a notification channel grouped into outputs
object. Also, view
, a Blessed.js object, is returned. It is the coordinator job to push it to the screen.
We have introduced to the coordinator another concept: the dispose bags. It may sound familiar to those who program in RxSwift: the dispose bag holds references to the subscriptions made by the view and unsubscribes all of them when it is thrown away. Unfortunately, the RxJS does not implement it. However, there is a trick: subscriptions can be added to other subscription. When the parent subscription is unsubscribed, all children as unsubscribed as well. In our implementation, the dispose bag is attached to the view pushed to the screen and is injected into both View Model and View. When the view is removed from the screen, the dispose bag is unsubscribed. This allows us to keep track of what subscriptions are associated with what view.
There is also one more thing to be noticed: the View Model associated with instantiated View is injected into it. This allows injecting dependencies into the View Model without involving the View: the View does not need to know anything about what View Model is going to use internally.
The View and View Model
Let’s see what is going on inside the loginView
(the registerView
is analogous, so it does not need to be covered):
The loginView
initializes the view and its controls. Then it passes observables created from the inputs to the View Model and collects outputs. Finally, the subscription to error message is created and the outputs relevant to the coordinator (in this case the observable that emits JWT token returned by the API). Also, this View will ask the coordinator to be dismissed when the user presses Escape key.
The View Model for login view is also simple:
The login view model simply takes inputs provided by the view as well as observable that emits value when the user hits the submit button. Its responsibility is to send the request to the API to verify user credentials. This is achieved with two utility functions: mapForm
and submitForm
:
The most important property of loginView
and loginViewModel
it the fact that neither of them knows nothing about each other. The only shared knowledge is the inputs and the outputs that each of them needs to do their job. This results in the extremely low coupling between the components. As a result, they may be developed in complete isolation. The testability is also greatly improved: they are just pure functions that accept arguments and return results. Creating unit tests in such a case is more than pleasant.
Conclusions
In our proof of concept the components turned out to be less coupled than in the original Coordinator Pattern: the Views managed by the Coordinator don’t need to have a reference to the managing Coordinator (as in the original approach). As a result, the components only know what they need for completing their tasks. This decoupling results in greater testability and isolation of application components.
It is worth mentioning, that the presented solution completely removes state that the programmer should take care of. The application is modeled as data flows instead. This significantly reduces the mental load of the programmer when it comes to reason about the system. Also, this is another reason that the tests are much easier to design.
Possible improvements
Presented code is just rough-and-dirty proof of concept and it is far from being a production code. Several improvements could be made:
- Some sort of library for immutable data (like Immutable.js) could be used to increase security and make the transformations withing reactive streams easier.
- Usage of the library that embraces functional concepts (for instance: Ramda) could reduce boilerplate and increase the readability of introduced dependency injection mechanism.
Both improvements could be made “for free” using other technology. One that comes to our minds is Clojure(Script) with core.async
for reactivity. It’s something we have been wrapping our heads around lately and we will for sure share our impressions as soon as we try it out.
Thanks to Iwo Herka and Mateusz Papiernik.
If you have any questions or an idea we could help you with…
Let’s talk!ex-Makimo, Team Leader and Senior Software Engineer. In spare time entertains himself with fishkeeping and growing his own food. Likes lawn mowing a lot.