You may be thinking it's easy to write like that when it's just dummy functions. C'mon `add5`!
So, I'm going to go through a login function for a chat app.
Mock Requirements:
1. User clicks login.
2. Modal prompts for `user` and `pass` fields.
2a. User submits form.
2b. User clicks 'forgot password'.
3. Upon success, load app+msg data for logged-in user.
3a. Cache ajax data for offline use.
3b. Update user status to 'online'
4. Upon failure, plunge into fire pit.
5. Show appropriate UI messaging
Using bluebird's unique take on "Promises," I'll show the code in *almost* as many lines as the (terse) requirements above.
chatApp.login = () => openLoginModal()
.then({user, pass}) => ajaxLogin({user, pass}))
.then(user => {
setUserStatus('online') // < non blocking, as result is irrelevant.
return [getContacts(user), getRooms(user), getMessages(user)]
})
.then(([contacts, rooms, messages]) => {
alertOnNew({contacts, messages})
return this.cache({contacts, rooms, messages})
})
.catch(UserCancel, hideModal)
.catch(ForgotPassword, () => showUserMessage({message: 'think harder'}))
.catch(LoginError, compose(hideModal, initFirePit, destroyApp))
.catch(err => showUserMessage({message: 'Something truly unexpected happened, congratulations.'}))
========
^^^ Code modified from a real-life app. Sadly the 'fire pit' feature is still in backlog.
I try organize succinct 'pathways' in my code.
Your Intent & flow must be fairly obvious. If your logic is 3 levels deep, nevermind across 3 files, you've lost 90% of "developers."
Good APIs are easily understood & implemented.
The Best APIs are built atop a stack of good APIs.
======== CONTINUED ========
You may have noticed something else about my code - 2 guiding principles (if not strict rules) for which I aim.
1. Eschew nested logic - a sign you are mixing 2+ different things. It's also a sign of untestable/high dimensionality.
2. More than 2 lines per function? Stop and think, or ask your nearest dev. Or you must be writing OpenSSL.
Ok, I'm being a bit of a troll about the '2 line limit.'
Here is what I'm getting at - we can gain more testable code if we refactor like so:
chatApp.getUserData = user => {
return [getContacts(user), getRooms(user), getMessages(user)]
}
chatApp.login = () => openLoginModal()
.then({user, pass}) => ajaxLogin({user, pass}))
.tap(() => setUserStatus('online'))
.then(chatApp.getUserData)
.tap(([contacts, rooms, messages]) => alertOnNew({contacts, messages}))
.then(this.cache)
.catch(UserCancel, hideModal)
.catch(ForgotPassword, () => showUserMessage({message: 'think harder'}))
.catch(LoginError, compose(hideModal, initFirePit, destroyApp))
.catch(err => showUserMessage({message: 'Something truly unexpected happened, congratulations.'}))
It's better.
`chatApp.getUserData` is now a testable function, instead of hidden inside the login() & coupled to the status update.
It's also flat.
Partitioned into 2 'sections' - .then/.tap, and then .catch's.
Errors can be filtered by type in bluebirds' .catch(<type>, <error>).
This creates a clear declaration of how your code ought to behave.
Either a .catch() function fired, or you get the result RETURNED from the final .then().
Break your tasks apart until they can resemble readable sequential chains.
Suddenly you will find you are at a place where the boundaries in your code self-organize.
You will find what "code reuse" *really* means. (In school I learned it's repeating the same bad pattern)
The clouds will clear, sun will shine, rainbows and ... you get the idea.
The more complex logic your app belongs in the farthest depths of your code tree.