Shindig: Patterns and Best Practices
- Previous article: Shindig: React Data Component
Now that we’ve covered how I built Shindig, I want to share some programming patterns that have helped me get through this huge project without losing my mind. The are just opinions, so there will definitely be people who disagree, but these are the things I wish someone told me two years ago.
1. Write all your code as pure functions without side-effects to the best of your ability.
This makes your code much more understandable. You should always be able to trace a variable from inputs to outputs through functions to understand what’s going on. When functions have side-effects, then you never really know what to expect from your code especially as it grows over time. Markup tends to be the biggest abuse of this principle because if you’re not intimately familiar with the markup, you’ll have no idea where variables are coming from. Here’s a simple example illustrating my point.
# non-pure example:
user = {first: 'Chet', last: 'Corcos'}fullname = ->
user.first + ' ' + user.lastconsole.log fullname()# pure example:fullname = ({first, last}) ->
first + ' ' + lastconsole.log fullname(user)
If this were a big project and you were debugging the non-pure example of fullname, then you might have no idea what its going to return. If you’re looking at where fullname is called, you might not be sure what closure its bound to and what the user variable is.
In the pure example, however, you only need to look at the inputs to the function to know whats going on. You don’t need to jump around the code looking for externalities. And when you call the function, you know what you’re going to get back because you passed the input right when you called it. So always use pure function and you’re live will be much better.
2. All data should be immutable.
This plays very nice with pure functions. Mutating objects can have unseen side-effects outside that function because you may be using the same object reference in multiple places. Trying to track down these kinds of bugs are a complete nightmare! So use Ramda.js and not Underscore and never mutate object references unless you have to. And if you have to use mutation, then hide that mutation behind an function with an immutable interface (like I did with the InstanceMixin).
Here’s a simple example illustrating how mutation can cause unintended consequences.
# mutable example
user = {first: 'Chet', last: 'Corcos'}reverse = (user) ->
user.first = user.first.split('').reverse().join('')
user.last = user.last.split('').reverse().join('')reversedUser = reverse(user)
console.log user.first + ' ' + user.last
console.log reversedUser.first + ' ' + reversedUser.last# simple immutable example
reverse = (u) ->
user = R.clone(u)
user.first = user.first.split('').reverse().join('')
user.last = user.last.split('').reverse().join('')# better immutable example (using Ramda)
reverse = R.evolve
first: R.reverse
last: R.reverse
In the mutable example, you’ve unknowingly changed the value of user outside of the function because you’re mutating the object reference. The two log statements will log the same exact thing. As your code gets more and more complicated, it may be really hard to track these bugs down. So never mutate objects. Preferably use a library like Ramda.js or Immutable.js or when in doubt, just clone the input objects so you don’t cause any side-effects.
3. Write thisless code.
In my experience, object oriented programming tends to overcomplicate code. Concepts like inheritance, polymorphism, decorators, context, and binding only convolute what’s really going on. All of this stuff can be accomplished with plain old functions and object primitives, so why not keep it simple? For example, these 23 lines allow you to cache Meteor subscriptions.
createSubsCache = (min) ->
obj = {}
obj.cache = {}
obj.subscribe = (name, arg) ->
key = JSON.stringify([name, arg])
sub = obj.cache[key]
if sub
sub.count++
Meteor.clearTimeout(sub.timerId)
delete sub.timerId
else
sub = Meteor.subscribe(name, arg)
sub.count = 1
ready: ->
sub.ready()
stop: ->
sub.count--
if sub.count is 0
sub.timerId = Meteor.setTimeout ->
sub.stop()
delete obj.cache[key]
, 1000*60*min
return obj
Now compare that to the object-oriented code that I wrote for ccorcos:subs-cache. I prefer the clarity of this pattern.
Lastly, I want to share some details about how to deploy your own Meteor app.
- Next article: Shindig: Deployment and DevOps