Open lukesutton opened 8 years ago
One of the major parts of achieving this design is to decompose parts of the application that are generally bound together. This is most obvious in the router. In most frameworks the router encodes the path, the method and the name of that path. Taken together they implicitly encode an action e.g. create a user, delete a blog post.
Instead the router can be broken up into separate parts:
Rendering is implicit. Rather than handlers explicitly choosing which template to render, a single render function is called. This render function inspects the state and decides which components should be rendered. A basic interaction could look like this:
ReadUserProfile
actionOne downside of this approach is that the render function needs to implicitly know about all the possible UI states. One way around this is to allow reducers to provide a rendering function. This is a view function which returns the relevant subsection. It gets passed off to the main render function, which just embeds it's output.
The above scheme can also easily accomodate rendering different content types e.g. JSON
There are precedents for this general approach. Firstly, the view layer can be compared to something like React. It is purely a set of nested components which act on their arguments. This doesn't seem that clever, since views should already be built in this way, but the truth is they end up being an ad-hoc mixture of views, partials and helper functions. The emphasis on purity is the difference and aids the general goal of testability.
While HTTP as a protocol is stateless, web-apps clearly aren't. They retrieve, store and update state. State is also preserved for the client using sessions. One repeated task in a web-application is collecting all the various bits of data required to render a view; this is derived from the URL and user session. One risk here is that the logic for collecting the data become spread out. An example is attempting to use RoR's
before_filter
to implement shared logic for looking up data. This ends up hurting however. Callbacks are not composable.One approach is to make the user-interactions in a web-application entirely state-full. This is the approach taken by the Seaside framework. All UI components have and keep track of state. User interactions which generally are async and multi-step — requests back and forth — instead appear to be synchronous since Seaside uses continuations.
Seaside eases certain interactions, but it's major downside is it's management of state. Objects are everywhere, with their own state which has to be managed. I also wonder about the testability of it, since it seems closely modelled on traditional desktop UIs, which tend to intermingle dependencies.
So, what if we looked to platforms like Elm? The major features of the Elm Architecture:
All of this contributes to a design focused on the unidirectional flow of data, where everything is represented as data.
How would a web-application use a similar model? Well, it can be quite similar, except that the flow of
action -> reduce -> render
is split over a web-request. Something likerequest -> action -> reduce -> render
. So, the crucial part here is that the app's view of client interactions is actions, not paths and HTTP methods; those are abstracted away.Then, instead of before_filters or the like, a series of functions are allowed to reduce over the existing state using the action and can decide what state to update or retrieve from external sources. Views are then rendered from that state.
The views can be constructed as a nested set of components, which operate on different sub-sets of that generated state.