Chapter 5
Composing objects
5.1 Being classical
data =
X100:
description: "A really cool camera"
stock: 5
X1:
description: "An awesome camera"
stock: 6
"X100: a camera (5 in stock)"
for own name, info of data
"#{name}: #{info.description} (#{info.stock} in stock)"
# ["X100: A really cool camera (5 in stock)",
# "X1: An awesome camera (6 in stock)"]
purchase = (product) ->
for own name, info of data
elem = document.createElement "li"
elem.innerHTML = "#{name}: #{info.description} (#{info.stock} in stock)"
elem.onclick = purchase name
Listing 5.1 A simple Camera
class
class Camera
constructor: (name, info) ->
@name = name
@info = info
render: ->
"#{@name}: #{@info.description} (#{@info.stock} in stock)"
purchase: ->
x1 = new Camera 'X1', {
description: 'An awesome camera', stock: 5
}
x1.name
#'X1'
x1.info
#{description: "An awesome camera", stock: 5}
x1.render()
# "Camera: X1: An Awesome camera (5)"
x1.render
# [Function]
class Shop
constructor: (data) ->
for name, info of data
new Camera name, info
data =
X100:
description: "A really cool camera"
stock: 5
X1:
description: "An awesome camera"
stock: 6
shop = new Shop data
> coffee 5.13.coffee 5.2
# => Visit http://localhost:8080/ in your browser
Listing 5.2 Agtron?s camera shop client application (version 1)
http = (method, src, callback) ->
handler = ->
if @readyState is 4 and @status is 200
unless @responseText is null
callback JSON.parse @responseText
client = new XMLHttpRequest
client.onreadystatechange = handler
client.open method, src
client.send()
get = (src, callback) ->
http "GET", src, callback
post = (src, callback) ->
http "POST", src, callback
class Camera
constructor: (name, info) ->
@name = name
@info = info
@view = document.createElement 'div'
@view.className = "camera"
document.body.appendChild @view
@view.onclick = =>
@purchase()
@render()
render: ->
@view.innerHTML = "#{@name} (#{@info.stock} stock)"
purchase: ->
if @info.stock > 0
post "/json/purchase/camera/#{@name}", (res) =>
if res.status is "success"
@info = res.update
@render()
class Shop
constructor: ->
get '/json/list/camera', (data) ->
for own name, info of data
new Camera name, info
shop = new Shop
5.2 Class inheritance
class Product
class Camera extends Product
class Robot extends Product
class Skateboard extends Product
class Product
constructor: (name, info) ->
@name = name
@info = info
render: ->
"#{@name}: #{@info.description} (#{@info.stock} in stock)"
purchase: ->
if @info.stock > 0
post "/json/purchase/camera/#{@name}", (res) =>
if res.status is "success"
@info = res.update
@render()
class Skateboard extends Product
skateOMatic = new Skateboard "Skate-o-matic", {
description: "It's a skateboard"
stock: 1
}
skateOMatic.render()
# Skate-o-matic: "It\'s a skateboard (1 in stock)"
skateOMatic = new Skateboard "Skate-o-matic", {
description: "It's a skateboard"
stock: 1
}
skateOMatic.render()
# Skate-o-matic: "It\'s a skateboard (1 in stock)"
class Camera extends Product
megapixels: ->
@info.megapixels || 'Unknown'
x11 = new Camera "x11", {
description: "The future of photography",
stock: 4,
megapixels: 20
}
sk8orama = new Skateboard "Sk8orama", {
description: "A trendy skateboard",
stock: 4
}
x11.megapixels?
# true
x11.megapixels()
# 20
sk8orama.megapixels?
# false
product =
render: ->
purchase: ->
construct = (prototype, name, info) ->
object = Object.create prototype
object.name = name
object.info = info
object
clock = construct product, 'clock', stock: 5
class Product
constructor: ->
render: ->
purchase: ->
camera = Object.create product
camera.megapixels = ->
x11 = construct camera, '', stock: 6
Listing 5.3 Agtron?s shop client application with multiple product categories
# http function omitted ? see listing 5.2
# get function omitted ? see listing 5.2
# post function omitted ? see listing 5.2
class Product
constructor: (name, info) ->
@name = name
@info = info
@view = document.createElement 'div'
@view.className = "product"
document.body.appendChild @view
@view.onclick = =>
@purchase()
@render()
render: ->
renderInfo = (key,val) ->
"<div>#{key}: #{val}</div>"
displayInfo = (renderInfo(key, val) for own key, val of @info)
@view.innerHTML = "#{@name} #{displayInfo.join ''}"
purchase: ->
if @info.stock > 0
post "/json/purchase/#{@purchaseCategory}/#{@name}", (res) =>
if res.status is "success"
@info = res.update
@render()
class Camera extends Product
purchaseCategory: 'camera'
megapixels: -> @info.megapixels || "Unknown"
class Skateboard extends Product
purchaseCategory: 'skateboard'
length: -> @info.length || "Unknown"
class Shop
constructor: ->
get '/json/list', (data) ->
for own category of data
for own name, info of data[category]
switch category
when 'camera'
new Camera name, info
when 'skateboard'
new Skateboard name, info
shop = new Shop
5.3 Class variables and properties
class Product
render: ->
instanceOfProduct = new Product
instanceOfProduct.render
Product.recall()
products = [
name: "Shark repellant"
,
name: "Duct tape"
]
find = (query) ->
(product for product in products when product.name is query)
find "Duct tape"
# [ { name: 'Duct tape' } ]
class Shop
constructor: (data) ->
products = []
for own name, info of data
products.push new Product(name, info)
Product.find('Pixelmator-X21')
# [ Camera { name="Pixelmator-X21", info={...} } ]
Product.find = (query) ->
(product for product in products when product.name is query)
class SecretAgent
secretWord = "antiquing"
secretWord?
# false
class SecretAgent
secretWord = "antiquing"
licensedToKill: yes
class SecretAgent
secretWord = "antiquing"
greet: (word) ->
if word is secretWord
"Hello, how are you?"
bob = new SecretAgent
bob.greet "antiquing"
# "Hello, how are you?"
class Product
products
Product.find = (query) ->
(product for product in products when product.name is query)
class Product
@find = (what) ->
"#{what} not found"
Product.find "zombie survival kit"
# "zombie survival kit not found"
class Product
instances = []
@find = (query) ->
(product for product in instances when product.name is query)
constructor: (name) ->
instances = instances.concat [@]
@name = name
new Product "Green", {}
Product.find 'Green'
# [ { name: 'Green' } ]
Listing 5.4 Agtron?s shop client application with find
# http, get and post functions omitted ? see listing 5.2
class Product
products = []
@find = (query) ->
for product in products
product.unmark()
for product in products when product.name is query
product.mark()
product
constructor: (name, info) ->
products.push @
@name = name
@info = info
@view = document.createElement 'div'
@view.className = "product"
document.body.appendChild @view
@view.onclick = =>
@purchase()
@render()
render: ->
show = ("<div>#{key}: #{val}</div>" for own key, val of @info).join ''
@view.innerHTML = "#{@name} #{show}"
purchase: ->
if @info.stock > 0
post "/json/purchase/#{@purchaseCategory}/#{@name}", (res) =>
if res.status is "success"
@info = res.update
@render()
mark: ->
@view.style.border = "1px solid black"
unmark: ->
@view.style.border = "none"
# class Camera omitted ? see listing 5.3
# class Skateboard omitted ? see listing 5.3
class Shop
constructor: ->
@view = document.createElement 'input'
@view.onchange = ->
Product.find @value
document.body.appendChild @view
@render()
get '/json/list', (data) ->
for own category of data
for own name, info of data[category]
switch category
when 'camera'
new Camera name, info
when 'skateboard'
new Skateboard name, info
render: ->
@view.innerHTML = ""
shop = new Shop
products = [
name: "Shark repellant"
,
name: "Duct tape"
]
class Product
Product.find = (query) ->
(product for product in products when product.name is query)
class Camera extends Product
Camera.find?
# true
class Product
class Camera extends Product
Product.find = (what) -> "#{what} not found"
Product.find?
# true
Camera.find?
# false
5.4 Overriding and super
class Human
Human.rights = ['Life', 'Liberty', 'the pursuit of happiness']
Human.rights
# ['Life', 'Liberty', 'the pursuit of happiness']
Human.rights = Human.rights.concat ['To party']
Human.rights
# ['Life', 'Liberty', 'the pursuit of happiness', 'To party']
class Gallery
constructor: ->
render: ->
class Camera
constructor: ->
@gallery = new Gallery
class Camera extends Product
render: ->
@view.innerHTML = """
#{@name}: #{@info.stock}
{@gallery.render()}
"""
class Camera extends Product
constructor: (name, info) ->
@name = name
@info = info
@gallery = new Gallery
render: ->
@view.innerHTML = """
#{@name}: #{@info.stock}
{@gallery.render()}
"""
class Product
Product.find = (query) ->
(product for product in products when product.name is query)
class Camera extends Product
x1 = new Camera 'X1', {}
Product.find 'X1'
# []
class Product
constructor: (name, cost) ->
@name = name
@cost = cost
price: ->
@cost
class Camera extends Product
markup = 2
price: ->
super()*markup
camera = new Camera 'X10', 10
camera.price()
# 20
Listing 5.5 Agtron?s shop client application with camera gallery
# http, get and post functions omitted from this listing
class Gallery
constructor: (@photos) ->
render: ->
images = for photo in @photos
"<li><img src='#{photo}' alt='sample photo' /></li>"
"<ul class='gallery'>#{images.join ''}</ul>"
class Product
constructor: (name, info) ->
@name = name
@info = info
@view = document.createElement 'div'
@view.className = 'product'
document.querySelector('.page').appendChild @view
@render()
render: ->
@view.innerHTML = "#{@name}: #{@info.stock}"
class Camera extends Product
constructor: (name, info) ->
@gallery = new Gallery info.gallery
super name, info
@view.className += ' camera'
render: ->
@view.innerHTML = """
#{@name} (#{@info.stock})
#{@gallery.render()}
"""
class Shop
constructor: ->
@view = document.createElement 'div'
document.querySelector('.page').appendChild @view
document.querySelector('.page').className += ' l55'
@render()
get '/json/list', (data) ->
for own category of data
for own name, info of data[category]
switch category
when 'camera'
new Camera name, info
else
new Product name, info
render: () ->
@view.innerHTML = ""
shop = new Shop
class Gallery
class Camera extends Product
constructor: (name, info) ->
@gallery = new Gallery
super
pixelmatic = new Camera 'The Pixelmatic 5000', {}
pixelmatic.name
# 'The Pixelmatic 5000'
5.5 Modifying prototypes
class Camera
render: ->
if /'Lacia'/.test @name
"Special deal"
Listing 5.6 CoffeeScript class and compilation to JavaScript
CoffeeScript
class Simple
constructor: ->
@name = "simple object"
simple = new Simple
|
JavaScript
var Simple = (function() {
function Simple() {
this.name = 'simple';
}
return Simple;
})();
simple = new Simple();
|
SteamShovel = (name) ->
@name = name
steamShovel = new SteamShovel 'Gus'
steamShovel.name
# 'Gus'
steamShovel.constructor
# [Function]
Listing 5.7 CoffeeScript class, constructor, and method compilation
CoffeeScript
class SteamShovel
constructor (name) ->
@name = name
speak: ->
"Hurry up!"
gus = new SteamShovel
gus.speak()
# Hurry up!
|
JavaScript
var SteamShovel = (function() {
function SteamShovel(name) {
this.name = name;
}
SteamShovel.prototype.speak =
function() {
return "Hurry up!";
};
return SteamShovel;
};
gus = new SteamShovel();
gus.speak();
|
SteamShovel.prototype = {}
SteamShovel.prototype.grumpy = yes
gus.grumpy
# true
class Example
example = new Example
Example.prototype.justAdded = -> "just added!"
example.justAdded()
# "just added!"
Example::justAdded = -> "just added!"
Example.prototype.justAdded = -> "just added!"
Listing 5.8 Agtron?s shop client application with specials
# http omitted from this listing
# get omitted from this listing
# post omitted from this listing
class Product
constructor: (name, info) ->
@name = name
@info = info
@view = document.createElement 'div'
@view.className = "product #{@category}"
document.querySelector('.page').appendChild @view
@view.onclick = =>
@purchase()
@render()
render: ->
@view.innerHTML = @template()
purchase: ->
if @info.stock > 0
post "/json/purchase/#{@category}/#{@name}", (res) =>
if res.status is "success"
@info = res.update
@render()
template: =>
"""
<h2>#{@name}</h2>
<dl class='info'>
<dt>Stock</dt>
<dd>#{@info.stock}</dd>
<dt>Specials?</dt>
<dd>#{@specials.join(',') || 'No'}</dd>
</dl>
"""
class Camera extends Product
category: 'camera'
megapixels: -> @info.megapixels || "Unknown"
class Skateboard extends Product
category: 'skateboard'
length: -> @info.length || "Unknown"
class Shop
constructor: ->
unless Product::specials?
Product::specials = []
@view = document.createElement 'div'
@render()
get '/json/list', (data) ->
for own category of data
for own name, info of data[category]
if info.special?
Product::specials.push info.special
switch category
when 'camera'
new Camera name, info
when 'skateboard'
new Skateboard name, info
render: ->
@view.innerHTML = ""
shop = new Shop
Listing 5.9 Product listings with specials
{
'camera': {
'Fuji-X100': {
'description': 'a camera',
'stock': 5,
'arrives': 'December 25, 2012 00:00',
'megapixels': 12.3
}
},
'skateboard': {
'Powell-Peralta': {
'description': 'a skateboard',
'stock': 3,
'arrives': 'December 25, 2012 00:00',
'length': '23.3 inches'
}
}
}
products =
camera:
'Fuji-X100':
description: 'a camera'
stock: 5
arrives: 'December 25, 2012 00:00'
megapixels: 12.3
skateboard:
'Powell-Peralta':
description: 'a skateboard'
stock: 3
arrives: 'December 25, 2012 00:00'
length: '23.3 inches'
5.6 Extending built-ins
object = {}
array = []
string = ''
object = new Object
array = new Array
string = new String
['yin','yang'].join 'and'
# 'yin and yang'
Array::join = -> "Array::join was redefined"
['yin','yang'].join 'and'
# "Array::join this was overridden"
"October 13, 1975 11:13:00"
new Date "October 13, 1975 11:13:00"
productAvailable = new Date "October 13, 1975 11:13:00"
productAvailable.daysFromToday()
Date::daysFromToday = ->
millisecondsInDay = 86400000
today = new Date
diff = @ - today
Math.floor diff/millisecondsInDay
christmas = new Date "December 25, 2012 00:00"
christmas.daysFromToday()
#339
Listing 5.10 Agtron?s shop client application with stock arrivals
# http omitted
# get omitted
# put omitted
Date::daysFromToday = ->
millisecondsInDay = 86400000
today = new Date
diff = @ - today
Math.floor diff/millisecondsInDay
class Product
products = []
@find = (query) ->
for product in products
product.unmark()
for product in products when product.name is query
product.mark()
product
constructor: (name, info) ->
products.push @
@name = name
@info = info
@view = document.createElement 'div'
@view.className = "product #{@category}"
document.querySelector('.page').appendChild @view
@view.onclick = =>
@purchase()
@render()
render: ->
@view.innerHTML = @template()
purchase: ->
if @info.stock > 0
post "/json/purchase/#{@purchaseCategory}/#{@name}", (res) =>
if res.status is "success"
@info = res.update
@render()
template: =>
"""
<h2>#{@name}</h2>
<dl class='info'>
<dt>Stock</dt> <dd>#{@info.stock}</dd>
<dt>New stock arrives in</dt>
<dd>#{new Date(@info.arrives).daysFromToday()} days</dd>
</dl>
"""
mark: ->
@view.style.border = "1px solid black";
unmark: ->
@view.style.border = "none";
class Camera extends Product
category: 'camera'
megapixels: -> @info.megapixels || "Unknown"
class Skateboard extends Product
category: 'skateboard'
length: -> @info.length || "Unknown"
class Shop
constructor: ->
unless Product::specials?
Product::specials = []
@view = document.createElement 'div'
@render()
get '/json/list', (data) ->
for own category of data
for own name, info of data[category]
if info.special?
Product::specials.push info.special
switch category
when 'camera'
new Camera name, info
when 'skateboard'
new Skateboard name, info
render: ->
@view = document.createElement 'div'
document.querySelector('.page').appendChild @view
@view.innerHTML = """
<form class='search'>
Search: <input id='search' type='text' />
<button id='go'>Go</button>
</form>
"""
@search = document.querySelector '#search'
@go = document.querySelector '#go'
@go.onclick = =>
Product.find @search.value
false
@search.onchange = ->
Product.find @value
false
shop = new Shop
class ExtendedDate extends Date
daysFromToday: ->
millisecondsInDay = 86400000
today = new Date
diff = @ - today
Math.floor diff/millisecondsInDay
5.7 Mixins
class Announcement
pigsFly = new Announcement
pigsFly.purchase()
# Error
class Renderer
class Product extends Renderer
class Camera extends Product
class Announcement extends Renderer
htmlRenderer =
render: ->
unless @view?
@view = document.createElement 'div'
document.body.appendChild @view
@view.innerHTML = """
#{@name}
#{@info}
"""
class Donut
constructor: (name,info) ->
@name = name
@info = info
Donut::render = htmlRenderer.render
dwarves =
bashful: -> 'Bashful'
doc: -> 'Doc'
dopey: -> 'Dopey'
class FairyTale
for key, value of dwarves
FairyTale::[key] = value
class Camera
include @, htmlRenderer
# ReferenceError: include is not defined
x1 = new Camera
x1.render()
#renders
include = (klass, module) ->
for key, value of module
klass::[key] = value
Listing 5.11 Mixin
class
class Mixin
constructor: (methods) ->
for name, body of methods
@[name] = body
include: (klass) ->
for key, value of @
klass::[key] = value
htmlRenderer = new Mixin
render: -> "rendered"
class Camera
htmlRenderer.include @
leica = new Camera()
leica.render()
#rendered
accumulate = (initial, numbers, accumulator) ->
total = initial or 0
for number in numbers
total = accumulator total, number
total
sum = (acc, current) -> acc + current
accumulate 0, [5,5,5], sum
# 15
class Product
instances = []
constructor: (stock) ->
instances.push @
@stock = stock
class Product
instances = []
constructor: (stock) ->
instances.push @
@stock = stock
stock: ->
stockAccumulator = (acc, current) -> acc + current.stock
accumulate(0, instances, stockAccumulator)
enumerable =
accumulate: (initial=0, accumulator, sequence) ->
total = initial
for element in sequence
total = accumulator total, element
total
include = (klass, module) ->
for key, value of module
klass[key] = value
class Product
include Product, enumerable
instances = []
accumulator = (acc, current) ->
acc + current.stock
@stockTotal = -> @accumulate(0, accumulator, instances)
constructor: (stock) ->
instances.push @
@stock = stock
trinkets = new Product 12
valium = new Product 8
laser = new Product 3
Product.stockTotal()
# 23
Object::antEater = ->
"I'm an ant eater!"
antFarm = new Product { stock:1 }
antFarm.antEater()
# "I'm an ant eater!"
include = (klass, module) ->
for own key, value of module
klass[key] = value
htmlRenderer = Object.create null
class Mixin
@:: = null
constructor: (from, to) ->
for key, val of from
to[key] = val
5.8 Putting it together
Listing 5.12 Agtron?s shop client application
server =
http: (method, src, callback) ->
handler = ->
if @readyState is 4 and @status is 200
unless @responseText is null
callback JSON.parse @responseText
client = new XMLHttpRequest
client.onreadystatechange = handler
client.open method, src
client.send()
get: (src, callback) ->
@http "GET", src, callback
post: (src, callback) ->
@http "POST", src, callback
class View
@:: = null
@include = (to, className) =>
for key, val of @
to::[key] = val
@handler = (event, fn) ->
@node[event] = fn
@update = ->
unless @node?
@node = document.createElement 'div'
@node.className = @constructor.name.toLowerCase()
document.querySelector('.page').appendChild @node
@node.innerHTML = @template()
class Product
View.include @
products = []
@find = (query) ->
(product for product in products when product.name is query)
constructor: (@name, @info) ->
products.push @
@template = =>
"""
#{@name}
"""
@update()
@handler "onclick", @purchase
purchase: =>
if @info.stock > 0
server.post "/json/purchase/#{@category}/#{@name}", (res) =>
if res.status is "success"
@info = res.update
@update()
class Camera extends Product
category: 'camera'
megapixels: -> @info.megapixels || "Unknown"
class Skateboard extends Product
category: 'skateboard'
length: -> @info.length || "Unknown"
class Shop
View.include @
constructor: ->
@template = ->
"<h1>News: #{@breakingNews}</h1>"
server.get '/json/news', (news) =>
@breakingNews = news.breaking
@update()
server.get '/json/list', (data) ->
for own category of data
for own name, info of data[category]
switch category
when 'camera'
new Camera name, info
when 'skateboard'
new Skateboard name, info
shop = new Shop
Listing 5.13 Agtron?s shop server application
http = require 'http'
url = require 'url'
coffee = require 'coffee-script'
data = require('./data').all
news = require('./news').all
script = "./#{process.argv[2]}.coffee"
client = ""
require('fs').readFile script, 'utf-8', (err, data) ->
if err then throw err
client = data
css = ""
require('fs').readFile './client.css', 'utf-8', (err, data) ->
if err then throw err
css = data
headers = (res, status, type) ->
res.writeHead status, 'Content-Type': "text/#{type}"
view = """
<!doctype html>
<head>
<title>Agtron's Cameras</title>
<link rel='stylesheet' href='/css/client.css'></link>
</head>
<body>
<script src='/js/client.js'></script>
</body>
</html>
"""
server = http.createServer (req, res) ->
path = url.parse(req.url).pathname
if req.method == "POST"
category = /^\/json\/purchase\/([^/]*)\/([^/]*)$/.exec(path)?[1]
item = /^\/json\/purchase\/([^/]*)\/([^/]*)$/.exec(path)?[2]
if category? and item? and data[category][item].stock > 0
data[category][item].stock -= 1
headers res, 200, 'json'
res.write JSON.stringify
status: 'success',
update: data[category][item]
else
res.write JSON.stringify
status: 'failure'
res.end()
return
switch path
when '/json/list'
headers res, 200, 'json'
res.end JSON.stringify data
when '/json/list/camera'
headers res, 200, 'json'
cameras = data.camera
res.end JSON.stringify data.camera
when '/json/news'
headers res, 200, 'json'
res.end JSON.stringify news
when '/js/client.js'
headers res, 200, 'javascript'
writeClientScript = (script) ->
res.end coffee.compile(script)
readClientScript writeClientScript
when '/css/client.css'
headers res, 200, 'css'
res.end css
when '/'
headers res, 200, 'html'
res.end view
else
if path.match /^\/images\/(.*)\.png$/gi
fs.readFile ".#{path}", (err, data) ->
if err
headers res, 404, 'image/png'
res.end()
else
headers res, 200, 'image/png'
res.end data, 'binary'
else
headers res, 404, 'html'
res.end '404'
server.listen 8080, '127.0.0.1', ->
console.log 'Visit http://localhost:8080/ in your browser'
5.9 Summary