Example: A video storefront
Let's take a look at an example. We're working on a video store front. The user can buy videos, and play videos that they own. If they aren't signed in, they need to sign in to buy a video, and we'd like to use modal dialogs for the sign in and purchasing process.
In order to determine if a user already owns the content, we need them to be signed in. It's also possible that upon signing in, they could already own the content they tried to purchase. This can quickly lead to an extremely complex set of conditionals, but we can simplify it by thinking about the problem like we would think about concurrency.
Making promises about user interactions
Any time we're dealing with user interaction, it is inherently asynchronous. In most code, this begins and ends with event handlers on individual DOM elements. This can be better abstracted if we think about the larger interactions a user can take, and represent that as a single asynchronous operation in the code.
A user may or may not be signed in. We could expose that, and let that conditional bubble throughout our code. However, most of the time, if a user isn't signed in, we're just going to prompt them to sign in. That means that rather than having a user who may or may not be signed in, we've always got a user that will be signed in at some point in the future. Let's look at how we might represent this in code:
class UserSession constructor: (@Q, @AuthModals, @Session) -> @_session = @Session.get() signIn: -> @_authDeferred = @Q.defer() @_resolveOrPrompt() @_authDeferred.promise completeSignIn: (data) -> @Session.save(data).then(@_completeAuth) _resolveOrPrompt: -> if @_session?.signedIn @_completeAuth(@_session) else @AuthModals.openSignIn() _completeAuth: (session) -> @_session = session @_authDeferred.resolve(session.user)
There's a lot going on here.
Q is Kris Kowal's Q library for working
with promises. We'll gloss over the implementation of
Session. We'll just assume that
AuthModals opens the modal dialog, and
something will call
completeSignIn when the form is submitted.
two things. It asks the server if we're already signed in, and will submit the
Ajax request to the server, and return a promise with the session object.
When code calls the
signIn method, we're returning a promise that will
eventually be resolved with the current user. If they're already signed in, we
resolve that promise immediately. Otherwise, we open the modal dialog for the
user to sign in, and resolve the promise once the form is submitted.
Good patterns are made to be built upon
This is incredibly powerful. We've completely removed the need for code to worry about whether or not the user is signed in for almost all cases. We can build upon this further by applying the same concept to purchases.
class UserPurchases constructor: (@Q, @UserSession, @PurchaseModals) -> purchase: (@video) => @UserSession.signIn().then(@_promisePurchase(video)) completePurchase: (license) @user.addLicense(@video, license) @_purchaseDeferred.resolve(license) _promisePurchase: (video) => (@user) => @_purchaseDeferred = @Q.defer() @_resolveOrPrompt(video) @_purchaseDeferred.promise _resolveOrPrompt: (video) => if @user.owns(video) @completePurchase(@user.licenseFor(video)) else @PurchaseModals.openSaleForm(video)
We apply the same pattern here. If a user already owns the video, we resolve the promise with the license immediately. If not, we prompt the user to buy it, and resolve it when the license is given to us later. A user can only buy one thing at a time. If the user closes the modal and tries to buy something else, the old promise is effectively thrown away.
Finally, we can hide all of this away inside of an object responsible for playing the video.
class VideoPlayer constructor: (@UserPurchases, @PlayerModal, @Video) -> play: (video) => @UserPurchases.purchase(video).then(@_loadAndPlay(video)) _loadAndPlay: (video) => (license) => @Video.load(video, license).then(@PlayerModal.openPlayer)
With this in place, code that wants to play a video needs only to call
VideoPlayer.play. It doesn't need to concern itself with whether or not the
user is signed in, or whether they already own the content. We would still need
to have a conditional inside of the template to decide whether the button
should say 'Buy' or 'Play'. However, if we use the Null Object
pattern, and give the template a
Guest when the user isn't
signed in, this still remains relatively simple.
By thinking about higher level user interactions the same way we would think about a larger asynchronous computation, we've avoided littering our code with ugly conditionals, and are able to isolate concerns like login and purchasing content to a single place. Testing each of these objects individually also becomes much easier than having to deal with the four cases in one place.
These kinds of situations are everywhere if you look for them. You just have to remember that asynchronicity isn't just for concurrency, and promises aren't just for XHR.
If you found this useful, you might also enjoy: