Future Object Modeling in ES6
The next specification of JavaScript, ES6, has a number of features that make the language less painful to work with. I wanted to explore how these new ES6 features can make working with object models more delightful.
The goal here is to make object modeling syntactically cleaner and easier to understand, requiring less mental overhead to work with. To this end, I built boodle, an experimental ODM that implements the features outlined here.
(Boodle works specifically with parse.com-backed data stores, but the thinking here can be applied to other storage mechanisms as well.)
Async Control Flow
Let’s dive into some samples for what current modeling libraries look like, and how we can improve them with ES6 features.
Consider a basic example of creating a User Session instance, then modifying, refreshing, and deleting it. Using a callback-centric library, this might look something like…
There are a few concerns here. The number of callbacks (and level of indentation) scales linearly with the number of async operations done — there are only four operations here and we already have ourselves a little callback pyramid. Error handling also looks like it could get messy here, and it’s generally not the most clear thing to read.
Using promises, we could get this into something like…
This feels better, but there’s still a lot of syntactic sugar involved. There’s also a good amount of cognitive overhead if you haven’t quite mastered the use of promises (i.e. how did fetchedSession get passed in line 15? how do we hold token in scope at line 13?)
Using generators and proxies in ES6, we can clean this up further…
No ascending indentation, and no pesky promise closures! This feels much cleaner and easier to reason about than both callbacks and promises; the syntax here is nearly identical to what a synchronous version of the function might look like, save for a few yield statements. Further, we’ve done something pretty cool on lines 5 and 12: directly get and set model properties without having to do something like get() or set(). We’ll get further into how the yield statements and direct accesses are implemented below.
Model Definitions
Getting into our implementation, let’s first talk about what our model definitions would look like.
Generally, definitions in existing libraries are not quite idiomatic to how we’d work with plain old JavaScript objects, and are fairly library-specific. With mongoose for instance, our Session model might look something like…
This is okay, but not completely satisfying. Instance and private methods are put in non-idiomatic properties, and it seems unlikely that we’d be able to create a model definition without keeping a page of documentation open. (Though, perhaps a forcing function for reading the docs is a good thing...)
With the new class definition syntax in ES6, we can do a little better:
It’s worth noting that the syntax here would be considered more or less, idiomatic ES6; although it’s not the standard now, the class syntax should gain adoption future tending.
Our syntax for instance/class methods becomes a little neater here. Unfortunately, using a getter for definition to get the model definition leaves something to be desired, in both syntax and semantics. It’s unclear to me if there’s a better solution for this, but it seems that in the current class syntax proposal,
There is (intentionally) no direct declarative way to define either prototype data properties (other than methods) class properties, or instance property [sic]
For now, this solution for model property definitions will have to do.
Generator-based control flow and yield
The yield keyword in ES6 allows us to pause and resume a function (technically, generator) and wait for an async process to complete. Concretely, this means that we can yield to promises returned by our models, which will pause execution (in a non-blocking fashion) until the promise gets resolved.
In the boodle BaseModel, the yieldable load() function looks something like…
We’re still using promises internally here, but yield lets us hide that complexity from client code.
Directly getting and setting properties
One last feature we can pick up from ES6 is proxies. Proxies allow us some amount of metaprogramming, similar to what we might use method_missing for in Ruby. The proxy wrapping function below intercepts getters and setters to automagically do the right thing when it comes to manipulating model properties.
This function will let us intercept when a model property like expirationTime is get or set, and do any complex type or sanity checks we might want. In this example for boodle, we simply forward it to Parse’s get() and set() functions if we find it in our model definition. If the property requested isn’t in the model, we do the default get/set behavior.
(Note that this also means we’ll need to wrap any given instance of the model inside of a proxy for these to work — client code will actually be interacting with the proxy, rather than the model directly. Boodle handles this by wrapping instances before they’re passed to client code.)
In sum
With ES6 generators, classes, and proxies, we’ve been able to clean up much of the syntactical and mental overhead generally involved with current object modeling libraries. The client code required to work with Boodle is much easier to follow along, and fits well into existing generator-based packages like Koa.
Though all of this code works, it’s not quite viable for any production environments yet — the ES6 spec is still in some amount of flux, and all of the above code needs a transpiler, shim, and the node.js harmony flag to work correctly. Nevertheless, the exploration here is a good sample for some of the neat things we’d be able to do as ES6 gains adoption. As mentioned prior, the boodle package implements much of these ideas and is available to play with.
See something that’s incorrect or could be done better here? Feel free to send along feedback!
You should follow me on twitter.