Facet based development with MobX
Introduction
When we program a React application we use existing components and libraries such as drop down buttons, state managers and HTTP clients. These components are fairly basic in the sense that they do not determine how the application behaves. For example, there is no reusable component to ensure that selected items also get highlighted, even though many applications need this kind of behaviour. This article introduces my own approach for programming reusable behaviours. But first I will describe why two popular ways to make behaviours reusable have not been very successful. I then briefly explain two approaches that inspired my own approach: Template Methods and what I will call Smart Containers. From there I will outline the requirements for my approach, and explain how they are met. The source code for my library (called SkandhaJS) can be found here and here.
Approaches that fail to enable reusable behaviour
Reusing behaviour using frameworks
Frameworks offer not just components but also behaviours such as selection, highlighting, drag-and-drop, etc. They are considered to be useful but also limiting. The reason is that as long as you follow the patterns offered by the framework then things work fine, but customizing the behaviour usually amounts to breaking into a black box. Trying to go against the expectation of an existing module - which is what “breaking into something” is all about - tends to have unexpected consequences and leads to code that is hard to understand and maintain.
Reusing behaviour using OOP
As Rich Hickey observed, Object Oriented Programmed (which, by the way, I do like) has failed to deliver on its promise of making code reusable. Suppose you have a DoFoo class. You want to add a DoBar class and you see the potential to reuse DoFoo. Usually DoFoo will have code that is specifically there to support Foo and gets in the way of DoBar. You can try to make a common base-class that has only the “useful common bits” that serve both the purpose of DoFoo and DoBar. This will work if neither DoFoo nor DoBar needs to break into these common bits, otherwise you are back to square one. Moreover, it often happens that the base class is just an ad-hoc collection of “useful bits” that has no easily recognizable purpose. Such classes make a code-base hard to understand and maintain.
Approaches that inspired SkandhaJS
Template Method
Template Methods were successfully used in the Erlang language to create reusable behaviours. In this design pattern there is a template that dictates the order of the main steps in a procedure, and callback functions that provide the implementation of these steps. For example, in the pseudo code below the password is verified first, and forwarding the user to the next page comes after:
def goToProtectedPage():
response = callbacks.verifyPassword();
if (response.success) {
callbacks.goToNextPage();
}
This looks superfically similar to OOP where member functions can be overridden, but there is an important difference that has to do with granularity. Callback functions are fine grained: they can be mixed and matched. With OOP, every time you override a class you create a new type, without the possibility to match bits and pieces of these classes at will. Moreover, adding a type increases the complexity of the application. With a Template Method you are free to use any combination of callback functions, without the need to add types.
Smart Containers
In this approach that I read about many years ago (in a white-paper that I unfortunately cannot find anymore), every module exists in a container. Each module can detect and use the other modules in the container. For example, a ShowPage module can detect a PasswordVerifier module in its container and use it before showing a page to the user. This allows you as a programmer to reuse behaviours by mixing and matching the right modules in your container. I don’t claim that this is necessarily a good way to create applications, but I found the idea to be inspirational: modules can detect other modules in their direct neighborhood and adjust their behaviour accordingly.
Outline of my approach
In this section I will outline my approach for creating reusable behaviours. Keep in mind that an application needs to be designed such that it can support reusable behaviours. In other words, you cannot take any existing application and put reusable behaviours on top. To allow for reusable behaviours, I’m using the following application requirements:
-
the application stores an Application State in its Application Layer which is separate from its Presentation Layer. The Application State contains information that is needed in several places in the application (e.g. the id of the currently selected document). On the other hand, if information is local to a visual component or its direct children then I may store it in the Presentation layer.
-
the Presentation Layer is relatively dumb. It visualizes the Application State and responds to user actions by sending commands to the Application Layer. The effects of this action (which determines the behaviour of the application) is decided by the Application Layer.
-
the Application State is stored in smart Containers, such as a Documents container and a Users container. I will explain below what “smart” means, but for now imagine that a Container with selection and highlight information can automatically synchronize the two.
-
smart Containers contain reusable parts called Facets. You can think of Facets as the different concerns that the Container takes care of. For example, a Users Container may have a Selection Facet, a Highlight Facet and a Filtering Facet. In general we try to use abstraction in Facets to make them reusable (a User selection and a Document selection can use the same abstraction)
-
Facets support operations that follow the Template Method pattern. For example, as a programmer you can customize the callbacks of the “selectItem” operation in the Selection Facet. We will see how combining different behaviours (e.g. after selecting an item in the Selection Facet, also highlight it in the Highlight Facet) is achieved via this callback mechanism.
Using containers in the Application Layer
Let’s now make these ideas concrete by looking at some code for the Users container. By the way, a complete example (and accompanying sample application repository) can be found in the README of SkandhaJS.
class UsersCtr {
@facet selection: Selection = new Selection();
@facet highlight: Highlight = new Highlight();
@facet filtering: Filtering = new Filtering();
@facet inputs: Inputs = new Inputs();
constructor() {
registerCtr({ // [1]
ctr: this,
initCtr: () => {
this._setCallbacks();
this._installPolicies();
}
);
}
_setCallbacks() {
const ctr = this;
setCallbacks(this.selection, { // [2,3]
selectItem: {
select(this: Selection_selectItem) {
handleSelectItem(this.selectionParams);
highlightFollowsSelection(ctr.selection, this.selectionParams);
}
]
})
setCallbacks(this.highlight, {
highlightItem: {
highlightItem(this: Highlight_highlightItem) {
handleHighlightItem(ctr.highlight, this.id)
}
}
})
// other actions omitted
}
_installPolicies() { // [4]
mapData(
[Selection, 'selectableIds'],
[Inputs, 'userById'],
getIds
)(this);
convertSelectedIdsToItems( // [5]
[Inputs, 'itemById']
)(this);
// other policies omitted
}
};
Notes:
-
the
registerCtr
function ties the facet instances to the container. To get the container instance from the facet instance you can usegetCtr(facet)
. -
the
setCallbacks
function installs callback functions for the operations of theSelection
andHighlight
facets. TheselectItem
callback function callshandleSelectItem
to calculate the next selection. It also callshighlightFollowsSelection
to ensure that selected items are also highlighted. -
The
highlightFollowsSelection
function updates the highlighted item. It is implemented as follows:export function highlightFollowsSelection( this: Selection, { itemId, isShift, isCtrl } ) { const ctr = getCtr(this); Highlight.get(ctr).id = itemId; }
-
the
_installPolicies()
function sets up additional rules inside the container. Often these rules declare data mappings that route information from one facet to the other. In the example, we see thatInputs.userById
is mapped ontoSelection.selectableIds
, using thegetIds
function to convert users into ids. -
The second policy (
convertSelectedIdsToItems
) mapsthis.selection.ids
ontothis.selection.items
usingthis.inputs.itemById
as a lookup table (whereget
is a static member function of the facet class). It is implemented as:
mapDatas(
[Selection, "items"],
[
[Inputs, itemById],
[Selection, "ids"],
],
(itemById: any, ids: any) => lookUp(ids, itemById)
)(this);
A closer look at the Selection facet
Now that you have an impression of how a container is setup, let’s look at one of the facets. The Selection
facet is
implemented as follows:
class Selection_selectItem {
selectionParams: SelectionParamsT;
select() {}
}
export class Selection {
@input selectableIds?: Array<any>;
@observable @data ids: Array<any> = []; // [1]
@observable @data anchorId: any;
@output items?: Array<any>;
@operation @host selectItem(selectionParams: SelectionParamsT) { // [2-5]
return (cbs: Selection_selectItem) => {
const { itemId, isShift, isCtrl } = selectionParams;
if (!this.selectableIds.includes(itemId)) {
throw Error(`Not a selectable id: ${itemId}`);
}
cbs.select();
}
}
static get = (ctr: any): Selection => ctr.selection;
}
Notes:
-
The
@observable
decorator comes from MobX. It allows clients to react automatically to changes in these members. -
The
selectItem
function is an operation that verifies theitemId
argument and then calls the “select” callback. The callbacks are installed via the@host
decorator that comes from a library called Aspiration that deals with aspect oriented programming.
Discussion
In the example we saw that highlighting is synchronized with selection even though the Selection
facet has no knowledge of the Highlight
facet. This is a key property of the proposed approach: facets can interact without knowing about each other. These interactions are
orchestrated in the _setCallbacks
and _installPolicies
functions of the container. The _setCallbacks
function installs callback
functions that have access to the entire container, which allows them to access and update multiple facets. The _installPolicies
function
installs mapping functions (and possibly other interactions) that map data from one facet to the other. So although this is not truly a Smart
Container that magically detects the components that live inside it, the container still plays the role of a mediator and orchestrator.
This article only offers a small introduction, skipping over many of the details. This approach based on containers and facets is both
ambitious and experimental, but so far I’ve found that it produces good results, and I welcome you to try it out.