Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 199 lines (139 sloc) 11.473 kb
3292cde Reg Braithwaite better links on each page
raganwald authored
1 Separating Concerns in CoffeeScript using Aspect-Oriented Programming
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
2 ---
3
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
4 Modern object-oriented software design [favours composition over inheritance][coi] and celebrates code that is [DRY][dry]. The idea is to separate each object's concerns and responsibility into separate units of code, each of which have a single responsibility. When two different types of objects share the same functionality, they do not repeat their implementation, instead they share their implementation.
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
5
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
6 [coi]: https://en.wikipedia.org/wiki/Composition_over_inheritance
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
7 [dry]: http://en.wikipedia.org/wiki/Don't_repeat_yourself
8
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
9 When composing functionality at the method level of granularity, techniques such as mixins and delegation are effective design tools. But at a finer level of granularity, we sometimes wish to share functionality within methods. In a traditional design, we have to extract the shared functionality into a separate method that is called by other methods.
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
10
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
11 **decomposing methods**
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
12
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
13 You might think of extracting smaller methods from bigger methods as *decomposing* methods. You break them into smaller pieces, and thus you can share functionality or rearrange the pieces so that your code is organized by responsibility.
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
14
a7f801c Reg Braithwaite fixed some bogosity
raganwald authored
15 For example, let's say that we are writing a game for the nostalgia market, and we wish to use partially constructed objects to save resources. When we go to actually use the object, we *hydrate* it, loading the complete object from persistent storage. This is a coarse kind of *lazy evaluation*.
16
17 Here's some bogus code:
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
18
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
19 class Wumpus
20 roar: ->
21 # code that hydrates a Wumpus
22 # ...
23 # code that roars
24 # ...
25 run: ->
26 # code that hydrates a Wumpus
27 # ...
28 # code that runs
29 # ...
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
30
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
31 class Hunter
32 draw: (bow) ->
33 # code that hydrates a Hunter
34 # ...
35 # code that draws a bow
36 # ...
37 run: ->
38 # code that hydrates a Hunter
39 # ...
40 # code that runs
41 # ...
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
42
a7f801c Reg Braithwaite fixed some bogosity
raganwald authored
43 We can decompose it into this:
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
44
45 class Wumpus
46 roar: ->
47 hydrate(this)
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
48 # code that roars
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
49 # ...
50 run: ->
51 hydrate(this)
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
52 # code that runs
53 # ...
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
54
55 class Hunter
56 draw: (bow) ->
57 hydrate(this)
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
58 # code that draws a bow
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
59 # ...
60 run: ->
61 hydrate(this)
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
62 # code that runs
63 # ...
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
64
65 hydrate = (object) ->
66 # code that hydrates the object from storage
67
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
68 **composing methods**
69
70 On an ad hoc basis, decomposing methods is fine. But there is a subtle problem. Implementation tricks like hydrating objects, memoizing return values, or other performance tweaks are orthogonal to the mechanics of what methods like `roar` or `run` are supposed to do. So why is `hydrate(this)` in every method?
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
71
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
72 Now the obvious answer is, "Ok, it might be orthogonal to the main business of each method, but it's just one line." The trouble with this answer is that method decomposition doesn't scale. We need a line for hydration, a line or two for logging, a few lines for error handling, another for wrapping certain things in a transaction...
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
73
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
74 Even when each orthogonal concern is boiled down to just one line, you can end up having the orthogonal concerns take up more space than the main business. And that makes the code hard to read in practice. You don't believe me? take a look at just about every programming tutorial ever written. They almost always say "Hand waving over error handling and this and that" in their code examples, because they want to make the main business of the code clearer and easier to read.
75
76 We ought to do the same thing, move hydration, error handling, logging, transactions, and anything else orthogonal to the main business of a method out of the method. And we can.\
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
77
78 **method combinations**
79
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
80 Here's our code again, this time using the [YouAreDaChef][chef] library to provide *before combinations*:
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
81
82 [chef]: https://github.com/raganwald/YouAreDaChef
83
84 YouAreDaChef = require('YouAreDaChef.coffee').YouAreDaChef
85
86 class Wumpus
87 roar: ->
88 # ...
89 run: ->
90 #...
91
92 class Hunter
93 draw: (bow) ->
94 # ...
95 run: ->
96 #...
97
98 hydrate = (object) ->
99 # code that hydrates the object from storage
100
27af5d3 Reg Braithwaite better finish
raganwald authored
101 YouAreDaChef(Wumpus, Hunter)
102 .before 'roar', 'draw', 'run', () ->
103 hydrate(this)
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
104
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
105 Whenever the `roar`, `draw`, or `run` methods are called, YouAreDaChef calls `hydrate(this)` first. And the two concerns--How a Wumpus works and when it ought to be hydrated--are totally separated. This isn't a new idea, it's called [aspect-oriented programming][aop], and practitioners will describe what we're doing in terms of method advice and point cuts.
106
107 [aop]: http://en.wikipedia.org/wiki/Aspect-oriented_programming
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
108
109 Ruby on Rails programmers are familiar with this idea. If you have ever written any of the following, you were using Rails' built-in aspect-oriented programming support:
110
111 after_save
112 validates_each
113 alias_method_chain
114 before_filter
115
116 These and other features of Rails implement method advice, albeit in a very specific way tuned to portions of the Rails framework.
117
118 **the unwritten rule**
119
120 > There is an unwritten rule that says every Ruby programmer must, at some point, write his or her own AOP implementation --Avdi Grimm
121
122 Let's look at how YouAreTheChef works. Here's a simplified version of the code for the `before` combination:
123
124 YouAreDaChef: (clazzes...) ->
125 before: (method_names..., advice) ->
126 _.each method_names, (name) ->
127 _.each clazzes, (clazz) ->
128 if _.isFunction(clazz.prototype[name])
129 pointcut = clazz.prototype[name]
130 clazz.prototype[name] = (args...) ->
131 advice.apply(this, args)
132 pointcut.call(this, args)
133
134 This is really simple, we are composing a method with a function. The method already defined in the class is called the *pointcut*, and the function we are supplying is called the *advice*. Unlike a purely functional combinator, we are only executing the advice for side-effects, not for its result. But in object-oriented imperative programming, that's usually what we want.
135
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
136 **other method combinations**
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
137
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
138 That looks handy. But we also want an _after method_, a way to compose methods in the other order. Good news, the after combination is exactly what we want. After combinations are very handy for things like logging method calls or cleaning things up.
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
139
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
140 But there's another great use for after combinators, triggering events. Event triggering code is often very decoupled from method logic: The whole point of events is to invert control so that an object like a `Wumpus` doesn't need to know which objects want to do something after it moves. For example, a Backbone.js view might be observing the Wumpus and wish to update itself when the Wumpus moves:
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
141
27af5d3 Reg Braithwaite better finish
raganwald authored
142 YouAreDaChef(Wumpus, Hunter)
143 .after 'run', () ->
144 this.trigger 'move', this
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
145
27af5d3 Reg Braithwaite better finish
raganwald authored
146 CaveView = Backbone.View.extend
147 initialize: ->
148 # ...
149 @model.bind 'move', @wumpusMoved
150 wumpusMoved: (wumpus) ->
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
151 # ...
152
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
153 The code coupling the view to the model has now been separated from the code defining the model itself.
154
155 YouAreDaChef also provides other mechanisms for separating concerns. *Around combinations* (also called around advice) are a very general-purpose combinator. With an around combination, the original method (the pointcut) is passed to the advice function as a parameter, allowing it to be called at any time.
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
156
27af5d3 Reg Braithwaite better finish
raganwald authored
157 Around advice is useful for wrapping methods. Using an around combinator, you could bake error handling and transactions into methods without encumbering their code with implementation details. In this example, we define the methods to be matched using a regular expression, and YouAreDaChef passes the result of the match to the advice function, which wraps them in a transaction and adds some logging:
158
159 class EnterpriseyLegume
160 setId: (@id) ->
161 setName: (@name) ->
162 setDepartment: (@department) ->
163 setCostCentre: (@costCentre) ->
164
165 YouAreDaChef(EnterpriseyLegume)
166
167 .around /set(.*)/, (pointcut, match, value) ->
168 performTransaction () ->
169 writeToLog "#{match[1]}: #{value}"
170 pointcut(value)
171
172 **summary**
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
173
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
174 Method combinations are a technique for separating concerns when the level of granularity is smaller than a method. This makes the code DRY and removes the clutter of orthogonal responsibilities.
3adfbfa Reg Braithwaite coffeescript links
raganwald authored
175
176 [coffee]: http://coffeescript.org/
bc245f3 Reg Braithwaite another link at the bottom
raganwald authored
177
178 ---
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
179
662fc1e Reg Braithwaite removed the bluebird :-(
raganwald authored
180 This article is loosely based on [Aspect-Oriented Programming in Ruby using Combinator Birds][ruby], part of a series about combinatory logic and its application to Ruby programming: [Kestrels](http://github.com/raganwald/homoiconic/tree/master/2008-10-29/kestrel.markdown#readme), [The Thrush](http://github.com/raganwald/homoiconic/tree/master/2008-10-30/thrush.markdown#readme), [Songs of the Cardinal](http://github.com/raganwald/homoiconic/tree/master/2008-10-31/songs_of_the_cardinal.markdown#readme), [Quirky Birds and Meta-Syntactic Programming](http://github.com/raganwald/homoiconic/tree/master/2008-11-04/quirky_birds_and_meta_syntactic_programming.markdown#readme), [Aspect-Oriented Programming in Ruby using Combinator Birds](http://github.com/raganwald/homoiconic/tree/master/2008-11-07/from_birds_that_compose_to_method_advice.markdown#readme), [The Enchaining and Obdurate Kestrels](http://github.com/raganwald/homoiconic/tree/master/2008-11-12/the_obdurate_kestrel.md#readme), [Finding Joy in Combinators](http://github.com/raganwald/homoiconic/tree/master/2008-11-16/joy.md#readme), [Refactoring Methods with Recursive Combinators](http://github.com/raganwald/homoiconic/tree/master/2008-11-23/recursive_combinators.md#readme), [Practical Recursive Combinators](http://github.com/raganwald/homoiconic/tree/master/2008-11-26/practical_recursive_combinators.md#readme), [The Hopelessly Egocentric Blog Post](http://github.com/raganwald/homoiconic/tree/master/2009-02-02/hopeless_egocentricity.md#readme), and [Wrapping Combinators](http://github.com/raganwald/homoiconic/tree/master/2009-06-29/wrapping_combinators.md#readme).
181
182 [ruby]: https://github.com/raganwald/homoiconic/blob/master/2008-11-07/from_birds_that_compose_to_method_advice.markdown#readme
fe43606 Reg Braithwaite YouAreDaChef
raganwald authored
183
7d4a695 Reg Braithwaite dasherize the mores
raganwald authored
184 ---
f69ce70 Reg Braithwaite Link to the book
raganwald authored
185
8c4fb37 Reg Braithwaite My recent work
raganwald authored
186 My recent work:
187
f9d4487 Reg Braithwaite allongé
raganwald authored
188 ![](http://i.minus.com/iL337yTdgFj7.png)[![JavaScript Allongé](http://i.minus.com/iW2E1A8M5UWe6.jpeg)](http://leanpub.com/javascript-allonge "JavaScript Allongé")![](http://i.minus.com/iL337yTdgFj7.png)[![CoffeeScript Ristretto](http://i.minus.com/iMmGxzIZkHSLD.jpeg)](http://leanpub.com/coffeescript-ristretto "CoffeeScript Ristretto")![](http://i.minus.com/iL337yTdgFj7.png)[![Kestrels, Quirky Birds, and Hopeless Egocentricity](http://i.minus.com/ibw1f1ARQ4bhi1.jpeg)](http://leanpub.com/combinators "Kestrels, Quirky Birds, and Hopeless Egocentricity")
4479986 Reg Braithwaite More recent work!
raganwald authored
189
f9d4487 Reg Braithwaite allongé
raganwald authored
190 * [JavaScript Allongé](http://leanpub.com/javascript-allonge), [CoffeeScript Ristretto](http://leanpub.com/coffeescript-ristretto), and my [other books](http://leanpub.com/u/raganwald).
21fcc00 Reg Braithwaite redirect to allong.es
raganwald authored
191 * [allong.es](http://allong.es), practical function combinators and decorators for JavaScript.
17be325 Reg Braithwaite revised footers
raganwald authored
192 * [Method Combinators](https://github.com/raganwald/method-combinators), a CoffeeScript/JavaScript library for writing method decorators, simply and easily.
4f7cce6 Reg Braithwaite githiub
raganwald authored
193 * [jQuery Combinators](http://github.com/raganwald/jquery-combinators), what else? A jQuery plugin for writing your own fluent, jQuery-like code.
f69ce70 Reg Braithwaite Link to the book
raganwald authored
194
16fa4bb Reg Braithwaite separated .sig
raganwald authored
195 ---
196
b2b5c4a Reg Braithwaite shuffle
raganwald authored
197 (Spot a bug or a spelling mistake? This is a Github repo, fork it and send me a pull request!)
198
7e71700 Reg Braithwaite updated links
raganwald authored
199 [Reg Braithwaite](http://braythwayt.com) | [@raganwald](http://twitter.com/raganwald)
Something went wrong with that request. Please try again.