Skip to content

Instantly share code, notes, and snippets.

@polotek
Created July 29, 2012 18:07
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save polotek/3200682 to your computer and use it in GitHub Desktop.
Save polotek/3200682 to your computer and use it in GitHub Desktop.
Some thoughts on a small project with Backbone and CoffeeScript

I spent some time on a little scripting project yesterday and decided to use Backbone and CoffeeScript. I've been looking for a reason to use Backbone just to become more familiar. I don't really use CoffeeScript, and even went through my periods as a CS hater. But it's always a good idea to stay familiar with what's out there. At the very least it'll make sure you're not full of shit when you talk to people about pros and cons. Anyway, this isn't comprehensive or even useful per se. I just thought I'd jot down some thoughts.

I should make a disclaimer that this wasn't a traditional project for Backbone. It was a node script that only uses models a collections. I needed to perform a bunch of actions on a dataset by hitting a REST api. I've been thinking for a while that tasks like this are often nicer if you can mock up a quick model domain around the api. Then you can work with Forum and Topic models and just do update and delete operations on them. Once you set these up, the scripting tasks become much more straight forward instead of wrangling http calls for everything.

I had read enough about Backbone to know the general outline of how to use Models and Collections. But trying to figure out how to treat these in CoffeeScript at the same time provided most of the headache. I wanted to try the class syntax, so I started out with something like this.

class Topic extends Backbone.Model
  url: ->
    baseURI + '/topics' + this.id

That seemed very reasonable to me. Yes the arrow function still bothers me and I was itching to use semi-colons. But all in all, pretty straight forward. Of course it didn't work. It's my own fault because I had written several of these along with a few other constructs without actually trying them out. I was just hacking and getting a feel for CS before worrying about getting the script running. I found myself very annoyed with CS. That's to be expected for me personally. I've talked about how the lack of punctuation and block delimiters actually slows me down. I have the same problem with Ruby and Python, and I used to write Python for a living. But it wasn't terrible, just personal preference, and I happen to be good at adapting to programming styles.

But here's a list of some things I noted.

I tried to use the string interpolation stuff.

class Topic extends Backbone.Model
  url: ->
    '#{baseURI}/topics/#{this.id}'

But it doesn't work with single quotes, which I prefer. I also used to write PHP so I know the advantages of being able to switch between strings that get interpolated and those that don't. But I also think that's mostly unnecessary in my experience. And this is supposed to be "just javascript". So finding that '' and "" now work differently is a smell to me. Not to mention that the string started to feel very cluttered with syntax once there are more than 2 substitutions. I went back to using concatenation.

I almost got used to the implicit return statements. It does require an extra moment of thought to look at the last line and make sure it's what I want returned from the function. I use return statements automatically so removing them is unlearning a habit. But once you do remove them, there's some dissonance with early return statements where you do have to use the keyword.

Topic = Zendesk.BaseModel.extend
  url: ->
    if this.has 'url'
      return this.get 'url'

    Zendesk.baseURI + '/topics/' + this.id

This is for the Zendesk api if you can't guess. Also I should probably use the little after conditionals if I'm trying to be idiomatic. I did in most places, but a few regular if statements crept in. Again the lack of delimiters makes this kind of bizarre to me.

  config: (config) ->
    extend this, config if config
    config

But the unless thing bothers me the most.

options.url = model.url() + '.json' unless options.url

It's not until I get to the end of the line that I learn this doesn't happen all the time. It's weird. It makes uncommon conditions look like first class statements. I may be missing something about this. But to be fair, I've been renewing my appreciation for simple if statements. I've even abandoned the && / || trick in some cases

callback null, data if callback // CS

// vs.

callback && callback(null, data); // JS shorthand

// vs.

if(callback) { return callback(null, data); }  // JS for old people

I've taken to sticking with the third form. Way more keystrokes, and yet completely unambiguous what's going on.

Finally, here's my little extend method. Backbone includes underscore, but I would've had to npm install it again to get it at the top level. And all I needed was this, so I wrote one. Can't say how many times I've done this exact thing. We really need this method or something very similar as a standard in JS. (end rant)

extend = (target, rest...) ->
  for obj in rest
    for key, val of obj
      target[key] = val

  target

I don't hate this. In fact I like this a lot. The rest params are sweet, the fact that for..in works on arrays is sweet. The fact that I can do a simple iteration over object properties is sweet. I've always been a fan of for loops over forEach, block scope not-withstanding. I haven't made my peace with for..of yet though. I would much rather have for..in just "do the right thing" depending on the input. But that's a pipe dream. Never gonna happen. So I could probably get used to the above. Or the JS equivalent anyway.

Anyway, back to getting the script working. I ran into several errors because my Model constructs weren't set up properly. I thought it was the class syntax. I realized that I didn't really know if class Topic extends Backbone.Model was equivalent to what the Backbone js tutorials say. var Topic = Backbone.Model.extend({}). My intuition told me it was not because I thought I remembered that class syntax in CS desugars to prototypal inheritance. It wouldn't be calling the extends method at all. So I skipped over this and switched to the assignment form. No classes. My models look like this in the end.

Topic = Zendesk.BaseModel.extend
  url: ->
    if this.has 'url'
      return this.get 'url'

    Zendesk.baseURI + '/topics/' + this.id

After the fact, I had the thought that Backbone.Model.extend was probably using the prototypal inheritance pattern underneath. That the class syntax was probably not my problem. But there you go.

The above problems were definitely harder to debug then they should've been. The error output doesn't point you to the right line. The code doesn't look the way I expect because it's generated. All the usual CS gripes. I won't spend a lot of time here because it just gets people riled up. I will say this though. I really feel like the only reason this wasn't a huge headache for me is because I'm very good at debugging javascript. I can't imagine someone who doesn't know javascript well going all in on CS and having to deal with this.

Anyway, I've got working models and collections now. Except they don't work because they expect to sync from the client using jQuery.ajax. One of the main reasons I wanted to try this is because I was intrigued at the idea of overriding the Backbone sync method and having all of i/o stuff just work. I used the backbone annotated source to get a feel for how sync usually works. And I ended up with this.

backboneSync = (method, model, options) ->
  method = Zendesk.methodMap[method]
  options ||= {}

  options.method = method
  options.url = model.url() + '.json' unless options.url
  options = extend {}, Zendesk.syncDefaults, options
  Zendesk.auth options

  success = options.success
  error = options.error

  request options, (err, req, body) ->
    return error(req, 'error', err) || err if err

    success body, 'success', req if success

This is also very reasonable. By putting this method on my Backbone models, I can load them and update them in a node script using the request module. This was pretty awesome and exactly what I wanted to get out of this project. There are some rough edges here though.

  • The success and error callbacks expect a jQuery.ajax style signature. I had to add the "success" and "error" things.
  • The node request and response objects are not compatible with the xhr object. So if some backbone code tries to use these, it'll probably blow up.
  • I had to add a method for authenticating. I would've thought Backbone supported a hook for this. I didn't see one offhand, but I didn't look too hard. Someone can enlighten me.
  • I had to copy the methodMap that maps from CRUD names to HTTP methods, e.g. "update" -> "PUT". This is an unnecessary step. This map should be exposed for anyone to use.

Okay, this went on much longer than I thought. I know it won't make much difference to those who want to argue with me about some of this stuff. But I should re-iterate this is all just my personal opinion and experience. Take with a grain of salt please. Feel free to use the code below. It's only what I needed and I didn't even need to finish the task I started. It's also probably broken in several places. Cheers.

request = require 'request'
Backbone = require 'backbone'
extend = (target, rest...) ->
for obj in rest
for key, val of obj
target[key] = val
target
backboneSync = (method, model, options) ->
method = Zendesk.methodMap[method]
options ||= {}
options.method = method
options.url = model.url() + '.json' unless options.url
options = extend {}, Zendesk.syncDefaults, options
Zendesk.auth options
success = options.success
error = options.error
request options, (err, req, body) ->
return error(req, 'error', err) || err if err
success body, 'success', req if success
Zendesk =
baseURI: 'https://company.zendesk.com/api/v2'
config: (config) ->
extend this, config if config
config
auth: (options) ->
auth = (encodeURIComponent this.username) + '/' +
'token:' + this.token
options.headers ||= {}
options.headers['Authorization'] = (new Buffer auth).toString 'base64'
options
BaseModel: Backbone.Model.extend
sync: backboneSync
parse: (body) ->
body = JSON.parse body
body.topic
syncDefaults:
method: 'GET'
json: false
methodMap:
create: 'POST'
update: 'PUT'
'delete': 'DELETE'
read: 'GET'
Comment = Zendesk.BaseModel.extend
url: ->
if this.has 'url'
return this.get 'url'
this.getTopic.url() + '/comments/' + this.id
getTopic: ->
this.collection.getTopic
Topic = Zendesk.BaseModel.extend
url: ->
if this.has 'url'
return this.get 'url'
Zendesk.baseURI + '/topics/' + this.id
Forum = Zendesk.BaseModel.extend
url: ->
if this.has 'url'
return this.get 'url'
Zendesk.baseURI + '/forums/' + this.id
Comments = Backbone.Collection.extend
model: Comment
url: ->
this.getTopic.url() + '/comments'
getTopic: ->
Topics.get this.topic_id
sync: backboneSync
Topics = Backbone.Collection.extend
model: Topic
url: ->
Zendesk.baseURI + '/topics'
sync: backboneSync
Forums = Backbone.Collection.extend
model: Forum
url: ->
Zendesk.baseURI + '/forums'
sync: backboneSync
module.exports =
Comment: Comment
Topic: Topic
Forum: Forum
Comments: Comments
Topics: Topics
Forums: Forums
config: Zendesk.config.bind Zendesk
@polotek
Copy link
Author

polotek commented Jul 29, 2012

I am interested in constructive pointers and input on this code. Either about idiomatic CoffeeScript or better Backbone techniques.

@mikeal
Copy link

mikeal commented Jul 29, 2012

what backbone and coffeescript both seem to encourage is this "classy" OO approach to structure and programming that I can't say I entirely agree with. it's a popular pattern so i'm not surprised this stuff is gaining traction but i'm just not a fan.

@tj
Copy link

tj commented Jul 29, 2012

callback null, data if callback looks absolutely terrible, it took me a few seconds to figure out that was even an invocation.

@pauljz
Copy link

pauljz commented Jul 29, 2012

With CoffeeScript I've grown fond of ignoring some of the "features" or syntactical sugar that I don't like. I have issues with some of the same things as you. I don't like the postfix conditionals except for in stupidly simple situations. Things like throw error if error come to mind. Most of the time I'll still write it out as two lines, even in a simple situation.

if error
    throw error

This is nice too as it always makes it easier to go in and add a clean else later. Doing an if and else on one line feels too dirty to me.

I also ignore the fact that you can do implicit returns. If my code is meant to return something, I find it much easier to make it clear that I am intending to return something.

I like CoffeeScript because of how clean and clear class declarations are, the concise syntax, especially for iteration, and ultimately, most of the things I don't like I can ignore, and write closer to "real" JavaScript.

Not being a Backbone user, I can't comment too much on the rest of it. My only constructive pointer here is to just do what feels right, even if it's not the most concise way to do it on CoffeeScript. Don't feel obligated to follow every nuance that it makes available to you, if you feel it's at the expense of readability.

(P.S. coming from a C++ background, double quotes are clearly the correct way to delimit a string. Just sayin'.)

@pauljz
Copy link

pauljz commented Jul 29, 2012

I agree with @visionmedia there too. I tend to go for the optional parentheses around function calls fairly often. I have to guess this greatly enhances readability to most coders.

@aredridel
Copy link

That looks pretty good, now that you've explained the coffescript awkwardness. Nothing about the backbone part sticks out as that odd to me.

@polotek
Copy link
Author

polotek commented Jul 29, 2012

@aredridel What's awkward about the coffeescript besides bailing on the class syntax?

@pauljz I think your approach is commendable. But the pressure to conform to idiomatic code is not a force to be dismissed. Writing code that roughly follows community conventions means a wider audience will be more familiar and comfortable with it. I suspect that the type of CS I would be comfortable writing would look horrible to most people. I feel the same way about people who write javascript like it's java.

@polotek
Copy link
Author

polotek commented Jul 30, 2012

A final word on CoffeeScript. My feeling is that it isn't things like classes that account for it's rising popularity as @mikeal suggests. Or at least that's a symptom and not the root cause. I think it's more about having a choice. Choosing the technology you want to use becomes a deeply personal thing for most programmers (those who actually have a choice). For a long time, there was no choice when it came to front-end programming. It was javascript or GTFO. But a lot of people aren't crazy about javascript for their own reasons. So when an alternative finally surfaced, lots of people were ready to give it more than a chance.

And honestly I don't think that's a bad thing at all. Choice of programming language is a great thing. But as @jesusabdullah said in a recently, my feeling is that CS doesn't pay the rent for me. There's a tradeoff to choosing it. Some people are very comfortable and even eager to make that tradeoff because they're fed up with js or they much prefer the feel of CS as a language. And that's great. For me, the downsides of CS far outweigh the benefits. But I no longer think it's important for anybody to agree with me on that.

The only thing that still bothers me about it is the idea that you can "avoid" CoffeeScript if you don't like it. It's the mantra everyone uses when defending CS from it's detractors; "If you don't like it, don't use it". But I think it's misguided. We can't avoid CS. Modern web development is a small world. We all deploy to the same environment and deal with the same constraints. As opposed to the server where a person who chooses ruby (gem, bundler, unicorn), can shape their environment very different from a person who chooses python (pip, fabric, WSGI). Everybody on the web is talking about the same things. About what's new and how best to move forward. There is so much awesome work being done. And up until recently, it was great that all you had to know to consume any of it was javascript.

But not anymore. Now I have to know CS too. I have to translate code samples in blog posts. I have to pull down CS-only repos and run them through the coffee compiler to include them in my project. Or I run into compiled javascript when looking through a project and realize that I'm going to have to go wade through the CS in order to get comfortable with the intent of the code. My main gripe with coffeescripters these days is that in giving themselves a choice of programming languages, they have taken away mine to a certain degree. Because the only real alternative I have is to ostracize their contributions. Yes they're technically interoperable, but sharing contributions isn't just about running code. Some portion of the work being contributed to web community will be closed to me, unless I learn CS. That bothers the shit out of me.

At the same time, I realize that's a very subjective and unhelpful view. As with most things, diversity (in language) may end up being a great thing for web development. I don't know. But it's also not great for me to deny that diversity to others out of some sense of propriety about javascript "owning" the web experience. Javascript the language doesn't own anything. It was the guy left standing the last time we asked the question of what we should program the browser with. But what's becoming more clear is that proponents of javascript are entering a battle to retain that right. I happen to like javascript a lot, and the people on top tend to develop a sense that they "belong" there somehow. But that's just not true. And we'll have to see if javascript comes out on top again. I may end up moving to something else eventually, because my fondness for my chosen language is trumped by my fondness for building awesome things with the best tools. I like to think that's true of most of us. So I think we all need to make sure we don't lose our ability to learn knew environments and evaluate them fairly.

@marlun
Copy link

marlun commented Jul 30, 2012

CoffeeScript just like most languages can look both gorgeous and incomprehensible depending on how you write it. I like both JavaScript and CoffeeScript but I've decided on JavaScript for now because of the easier debugging.

@tj
Copy link

tj commented Jul 30, 2012

shitty features enable shitty use, take proxies for example, we dont really need them, but they will generate a mess of terrible code. Removing as many tokens from a language as possible doesn't make you a language designer, nor does it solve real problems, it makes things ambiguous, difficult to read, perhaps increasing writing speed slightly (not that it actually matters), but slowing down perception. Devs will figure this out some time but it's certainly a curve, I used to like Ruby.

@iamnoah
Copy link

iamnoah commented Jul 30, 2012

"just do what feels right, even if it's not the most concise way to do it"

I wish we just said this more often in the community, instead of harping on how feature X or language Y is going to confuse the inexperienced/ignorant programmer. Don't accept pull requests that are bad code, try to write good code and don't hire people that can't write anything but bad code. There are lots of bad programmers in the world. You can't blame CS/PHP/JS/VB or whatever language you really hate for the fact that there are a lot of people writing really terrible code every day. On some days, I'm one of them. We all write bad code (although some more than others). I'd like to blame my tools, but it's almost always lack of familiarity with the tool. I can get mad because my drill did a terrible job of mitering*, but that wont make it any better for the wrong job.

CS is just one more tool. Maybe it's a drill, maybe it's a double clawed hammer, maybe it's a freaking awesome table saw. Whatever it is, it's going to have things it is good at and and things it isn't. I personally hate writing idiomatic jQuery in CS, but love it for lower level libraries and that sort of thing.

Above all we should not be afraid of new tools. A language is just another tool. People complained about learning jQuery when it was new. A lot of people still complain about node.js, functional programming, Deferreds, etc. We're really all just saying, "this doesn't look like my code; I don't like it." That doesn't mean you have to accept code in any language if it isn't a good fit, but we all probably need to learn CoffeeScript so we can say whether it is or isn't just the thing we need.

* the challenge with new programming tools is that unlike conventional tools, we often don't know what they're good for without a lot of experimenting. We're still trying to figure out what kind of tool CS is.

@rauschma
Copy link

@polotek: Well put. For me it is also about nativeness: If I program for iOS, I use Objective C. If I program for Android, I use Java. If I program for the web, I use JavaScript. Backtracking a little from the last statement, I was very happy with GWT for a long time (pre Node.js times), because I could write server and client code in the same language and because Java IDEs were (and are still) better than JavaScript IDEs.

@aredridel
Copy link

@iamnoah -- THIS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment