I was recently in a meeting with the CTO of a startup which had spent several years building an Angular1.x-based front-end to their Big Data offering. His advice:
However you end up architecting and implementing your front-end, do not code your business logic in your Web framework. All the business logic must be implemented in vanilla.js
Earlier this year, we discussed that topic on the SAM pattern Gitter room and Thomas J. Buhr concluded:
A good Front-End Architecture should enable you to pin your modularized functions to the UI components in the most decoupled way possible. That way the technology backing the components can be swapped out and all your business logic is not in a hostage situation (at the mercy of the next great late framework)
The question though is how do you achieve this kind of outcome?
The bread and butter of Front-End programming models are events and callbacks. For decades, people have coded their front-end by hooking up handlers to events without thinking too much about the proper factoring of a handler:
// function onMouseDown(event) { mouseDown = true rectangle = { from: event.position, to: event.position } } function onMouseMove(event) { if (mouseDown) { rectangle.to = event.position; draw(rectangle); } } function onMouseUp(event) { mouseDown = false rectangle = { from: event.position, to: event.position } } //
Even though this approach seems reasonable there are many issues that appear at scale, when your code base grows beyond a free hundreds lines of code and event processing needs to be concurrent (network and user events):
Composability | Event handlers cannot be easily grouped by UI components as events may serve different purpose |
Encapsulation | The programming model relies on global variables (such as mouseDown to implement the proper behavior |
Subscriptions | The implementation must constantly subscribe and unsubscribe to events in order to associate the correct handler to the correct event |
Separation of Concerns | An event handler implements all the code necessary to make the proper changes to the UI, from back-end calls to DOM manipulation |
Data consistency | As global variables are updated, glitches may appear as other events get processed before an update completes (as a unit of work) |
Maintainability | When this king of code grows beyond a few hundred lines of code it is hard to maintain. |
Modern Frameworks, including React or Angular2 tend to encourage a component model that mimics that traditional event / callback model. This approach is generally considered to address well the issues of composability, encapsulation and separation of concerns. For instance a React component that draws a rectangle would generally include the event handlers and the component underlying model.
// var Rectangle = React.createClass({ getInitialState: function() { ... } handleMouseDown: function(e) { ... } handleMouseUp: function() { ... } handleMouseMove: function(e) { //If we are dragging if (!this.dragging) { return; } e.preventDefault(); //Get mouse change differential var xDiff = this.coords.x - e.pageX, yDiff = this.coords.y - e.pageY; ...} render: function() { return ( <div onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onWheel={this.handleMouseWheel} > ... ... </div> ) ; //
The problem, however, is that now your business logic is trapped in the Framework code itself, sometimes within a strongly opinionated programming model that constrains the way you choose to factor it, in particular the way you invoke back-end APIs.
.@dan_abramov – where you recommend firing initial, 1-time action to fetch data? I thought compDidMount'd be good – in reality not so much.
— Adam Rackis (@AdamRackis) June 26, 2016
So, how can we keep the application business logic and the view components separate while addressing the original issue of composability, encapsulation, subscriptions…?
This is actually what the SAM pattern excels at. SAM’s programming model (derived from TLA+ and the Paxos protocol) is dedicated to keep your business logic outside your view components. The components themselves can be implemented with the Web framework of your choice and connected to the business logic as an interchangeable “theme”.
In SAM an event handler is decomposed in three independent roles:
– Proposers
– Acceptor
– Learner
In response to an event, actions create a proposal to mutate the application state. The acceptor (the model) accepts or rejects these values, as a unit of work, and once the mutation is complete, i.e. the acceptor has accepted the value, the learner is able to access the new application state and create a state representation while computing a potential next-action (an automatic action that must take place in the given application state).
Both the Actions and the State function act as adapters between the View and the Model. An application state mutation can only be achieved via an Action/Model/State step. No other action can be processed until the State function has access to the new application state (and could validate that the new action is in fact allowed in the give application state). SAM emphasized that the model (as a set of property values) is not representative of the application state, the “true” State of the system is a function of the model that is able to compute the allowed actions, potential next-actions and the State Representation (i.e. the View) itself.
The Mutation occurs as a “unit-of-work” based on the action’s proposal. Only the Model can mutate the application state by accepting (or rejecting) proposals from the actions. In order to compute the proposal (i.e. the property values the application state should mutate to), the action generally applies validation rules and may enrich the source event by calling specific APIs. For instance a “updateAddress” action will most certainly invoke a getPostalAddress API to propose the correct values to the Model, instead of the values entered by the user.
Composability | SAM offers an "assembly" model of loosely coupled elements, where events are wired to Actions, which trigger units of work in the Model, while the State function focuses on translating the application state into one or more device specific state representations. |
Encapsulation | Actions, Model and State encapsulate specific behaviors which can be reused an assembled in a variety of applications |
Subscriptions | SAM does not rely on subscriptions at all, the State function always computes the actions which are allowed |
Separation of Concerns | The Actions, the Model, the State and the View (State Representation) define clear roles and responsibilities |
Data Consistency | Data consistency is achieved via a "step" mechanism (Action->Model->State) and the mutation of the application state being controlled by the Model only, within a critical section (only one action at a time can be processed). |
Maintainability | All the elements of the pattern, Actions, Model, State and View components can be independently maintained. Their role and responsibilities make it easy to reason about them |
In SAM, Actions convert an event or intent, into a proposal, in general a data-centric proposal, if possible with the property values the Model should mutate to. In the example below you will note that the action translate an event into a unit-of-work request to the Model to create a new Rectangle.
// mouseDown(e) { var rect = {} ; rect.startX = e.pageX - this.offsetLeft; rect.startY = e.pageY - this.offsetTop; model.present({newRectangle: rect}) ; } //
The corresponding unit of work in the Model looks like this:
// present(data) { ... if (data.newRectangle) { this.rect = data.newRectangle || {startX: 0, startY: 0} ; this.rect.w = 0 ; this.rect.h = 0 ; this.selectedObjects = -1 ; } ... } //
This means that this unit-of-work can potentially be reused in a variety of situations, not just connected to a mouseDown event.
The next step on the SAM pattern is for the model to invoke the State function, which computes the state representation (What to display) and ask the view to display it (Where to display). The State function is also responsible for computing the next-action, in this case the action will be responsible for counting how many objects are in the rectangle, provided the application state is in that specific state:
// var stateRepresentation = { rect: draw( model.rect.startX, model.rect.startY, model.rect.w, model.rect.h, model.color), } view.display(stateRepresentation) ; // Next-Action if (model.countObjects) { this.actions.selectObjects(model.objectCount,model.rect) ; } //
Note that in the present case, we have chosen the state representation to be function, but it could be some JSON structure passed as props to a react component or even an HTML string mounted in the page via an innerHTML statement.
Last, the view converts the state representation into view components (How to display):
// display(stateRepresentation) { this.theme.rectangle( { rect: stateRepresentation.rect } ) ; } //
Here is the rectangle component.
// rectangle(rect) { // draw the rectangle // in this case, the state representation is a function rect.rect(this.ctx,this.canvas) ; } //
The whole sample (as a “fishing” game) can be found here.
Front-End architectures are evolving very rapidly with component-based frameworks like React and Angular2. Our word of caution is that these frameworks present many benefits and should be part of your Front-End architecture. However, we suggest that you keep your business logic entirely separate. The SAM pattern is a great way to achieve that outcome, while retaining all the benefits of modern front-end architectures: from composability to maintainability.