Dependency Injection with CoffeeScript

May 22, 2014

We've all been there before.

You're trying to write and test CoffeeScript UI code and you've hacked up the window object, and used global scope as a workaround to avoid passing every little piece of data around to all of your classes. Say you need a session object that every controller in your app needs. Why add it to all of those constructors when you can just tack it on the window or some other global object?

Because you'd be building a house of cards.

But is it worse than having classes whose constructor parameters span many lines?

CoffeeScript allows you to write JavaScript in an object-oriented style if you wish, so why rely on these haphazard methods when you could leverage existing OO patterns?

Forget that you've ever heard of dependency injection for a moment. Depending on your experience and the frameworks you've used, this could prove to be a very pleasurable experience.

Over the years, the term "dependency injection" has meant different things to different people. We'll try to strip it down here to its basic form. We'll use CoffeeScript as an example language because first, we have a little library to show off, and second, who cares?

Dependency injection is not a library or a feature of a framework. It's a pattern to use in OO programs to help your classes adhere to the Single responsibility principle. A class that has a single responsibility is an easier class to compose, and an easier class to test.

Here's a simple example of a MapController that will render a collection of locations to a map. You'll have to use your imagination to fill in the gaps.

class LocationCollection extends Collection


class LocationsMapView
  constructor: ->
    @locations = new LocationCollection()
    @template  = getTemplate('map-location')

  render: ->
    @renderCollection(@locations, @template)


class MapController
  constructor: ->
    @mapView = new LocationsMapView()

  render: ->
    @el.empty()
      .append(@mapView.render())


main = (root) ->
  mapController = new MapController()
  root.html(mapController.render())

main($('body'))

Easy, breezy, beautiful. But not yet Cover Girl. As you stare longingly at your wonderful new map, you realize that having a table listing of locations would really bring the room together. So, some kind of LocationsTableView, but it's going to need the same instance of LocationCollection, innit? Luckily, we're not programborn yesterday, so this is a pretty simple operation.

All it takes is letting MapController instantiate the LocationCollection instead of the LocationsMapView and passing that collection to both views.

class LocationCollection extends Collection


# Our new View
class LocationsTableView
  constructor: (@locations) ->
    @template = getTemplate('location-table')

  render: ->
    @renderCollection(@locations, @template)


# The old view now takes an instance of the locations
# collection instead of trying to create it
class LocationsMapView
  constructor: (@locations) ->
    @template  = getTemplate('map-location')

  render: ->
    @renderCollection(@locations, @template)


# The controller will now control the collection's scope
# with respect to the views
class MapController
  constructor: ->
    locations  = new LocationCollection()
    @mapView   = new LocationsMapView(locations)
    @tableView = new LocationsTableView(locations)

  render: ->
    @el.empty()
      .append(@mapView.render())
      .append(@tableView.render())


main = (root) ->
  mapController = new MapController()
  root.html(mapController.render())

main($('body'))

We can see where this is going. We'll add another controller that needs the location collection, and we'll keep pushing the instantiation up until all objects are created in what is effectively the main function. Let's round out the definitions and see how the classes look now that they're closer to following the single responsibility principle.

class LocationCollection extends Collection


# It would be nice to have the owner displayed in the table
# view
class LocationsTableView
  constructor: (@locations, @users) ->
    @template = getTemplate('location-table')

  render: ->
    for location in @locations
      user = @users.get(location.owner_id)
      @template(user: user, location: location)


# And we'll ask the geocoder service for a good street name
# for each location rendered
class LocationsMapView
  constructor: (@locations, @geocoder) ->
    @template  = getTemplate('map-location')

  render: ->
    for location in @locations
      street = @geocoder.getStreet(location)
      @template(street: street, location: location)


class MapController
  constructor: (locations, users, geocoder) ->
    @mapView   = new LocationsMapView(locations, geocoder)
    @tableView = new LocationsTableView(locations, users)

  render: ->
    @el.empty()
      .append(@mapView.render())
      .append(@tableView.render())


# Geez, now we need to string everything together in the
# main function
main = (root) ->
  locations  = new LocationCollection()
  users      = new UsersCollection()
  geocoder   = new Geocoder()

  # Ugh
  mapController = new MapController(locations, users, geocoder)
  root.html(mapController.render())

main($('body'))

That's a mess, and it's only going to get messier. MapController requires parameters that it should have nothing to do with. The reason that it needs those parameters is because the controller is concerning itself with the instantiation of the other classes. Because it handles multiple concerns, it now needs data unlreated to it. So, what does the map controller need? It shows a LocationsMapView and a LocationsTableView. Easy enough.

class LocationCollection extends Collection


# This hasn't changed
class LocationsTableView
  constructor: (@locations, @users) ->
    @template = getTemplate('location-table')

  render: ->
    for location in @locations
      user = @users.get(location.owner_id)
      @template(user: user, location: location)


# Neither has this
class LocationsMapView
  constructor: (@locations, @geocoder) ->
    @template  = getTemplate('map-location')

  render: ->
    for location in @locations
      street = @geocoder.getStreet(location)
      @template(street: street, location: location)


# MapController just requires the views it needs to do its
# job
class MapController
  constructor: (@mapView, @tableView) ->

  render: ->
    @el.empty()
      .append(@mapView.render())
      .append(@tableView.render())


# But this is getting complex
main = (root) ->
  locations  = new LocationCollection()
  users      = new UsersCollection()
  geocoder   = new Geocoder()

  mapView   = new LocationsMapView(locations, geocoder)
  tableView = new LocationsTableView(locations, users)

  mapController = new MapController(mapView, tableView)
  root.html(mapController.render())

main($('body'))

For each of those classes, ask yourself "what does this class do? What does it need to exist?" The controller strictly just needs those two views, and each view requires only what it needs to do its job.

This is the essence of dependency injection. It's not a hard and fast rule, but in general, if a class is making a new instance of another class, it's violating the single responsibility principle.

And that code kinda sucks. In particular, the junk at the bottom sucks. If the order of any construction parameters changed or were added, it would be a nightmare to hunt down. Then you'd have to trace through that weaving path of object instantiation.

And that's where the libraries come in. Wiring everything together is fickle and a pain.

(Semi-) Automated Dependency Injection

What dependency injection libraries do is help you string together your application in a way where each class can explicitly declare its dependencies, and you don't have to write an error prone string-em-together function.

Here's our example with honk-di.

inject = require 'honk-di'


class LocationCollection extends Collection
  # We only ever want one of these
  @scope: 'singleton'


class LocationsTableView

  # These dependencies will be provided for us. Their scope
  # or concrete implementation is not our concern.
  locations:  inject(LocationCollection)
  users:      inject(UsersCollection)

  # The constructor will still be called. @locations and
  # @users will be populated at this point
  constructor: ->
    @template = getTemplate('location-table')

  # This hasn't changed
  render: ->
    for location in @locations
      user = @users.get(location.owner_id)
      @template(user: user, location: location)


class LocationsMapView

  # Because LocationCollection has @scope: 'singleton', we
  # can rest assured this class will get the same instance
  # as the LocationsTableView
  locations:  inject(LocationCollection)
  geocoder:   inject(Geocoder)

  constructor: ->
    @template = getTemplate('map-location')

  render: ->
    for location in @locations
      street = @geocoder.getStreet(location)
      @template(street: street, location: location)


# MapController doesn't even need a constructor anymore.
# Everything it required has been provided
class MapController
  mapView:    inject(LocationsMapView)
  tableView:  inject(LocationsTableView)

  render: ->
    @el.empty()
      .append(@mapView.render())
      .append(@tableView.render())


# Okay, this has gotten cleaned up quite a bit
main = (root) ->
  injector = new inject.Injector()
  mapController = injector.getInstance(MapController)
  root.html(mapController.render())

main($('body'))

Each class declares its dependencies and isn't concerned with creating other classes. The logic of a class's scope is now declarative, and the concept of object creation has almost entirely been removed from the program.

This all kinda works by default without declaring any bindings. But, bindings will allow you to do neat tricks like having configuration values provided to classes, change scope of classes in different contexts, and bind implementations to parent classes. A little bit of practice with the concept will have you making lovely little testable classes.

The library, while nascent, is used in a large production application. More in-depth documentation is provided on its github page. Feedback and pull requests are very welcome.

And, as always, if you find any of this interesting, please do drop us a line.