4

What is the best way to dynamically create classes in CoffeeScript, in order to later instantiate objects of them?

I have found ways to do it, but I am not sure if there is maybe an even better (or simpler) way to achieve it. Please let me know your thoughts on my code.

Let's start with simple non-dynamic classes:

class Animal
  constructor: (@name) ->

  speak: ->
    alert "#{@name} says #{@sound}"

class Cat extends Animal
  constructor: (@name) ->
    @sound = "meow!"

garfield = new Cat "garfield"
garfield.speak()

As expected, garfield says meow!

But now we want to dynamically generate classes for more animals, which are defined as follows:

animalDefinitions = [
    kind:  'Mouse'
    sound: 'eek!'
  ,
    kind:  'Lion'
    sound: 'roar!'
  ]

The first naive attempt fails:

for animal in animalDefinitions
  animal.class = class extends Animal
    constructor: (@name) ->
      @sound = animal.sound

mutant = new animalDefinitions[0].class "mutant"
mutant.speak()

The animal we just created, mutant, should be a mouse. However, it says roar! This is because animal.sound only gets evaluated when we instantiate the class. Luckily, from JavaScript we know a proven way to solve this: a closure:

for animal in animalDefinitions
  makeClass = (sound) ->
    class extends Animal
      constructor: (@name) ->
        @sound = sound
  animal.class = makeClass(animal.sound)

mickey = new animalDefinitions[0].class "mickey"
mickey.speak()

simba = new animalDefinitions[1].class "simba"
simba.speak()

Now it works as desired, mickey mouse says eek! and simba the lion says roar! But it looks somewhat complicated already. I am wondering if there is an easier way to achieve this result, maybe by accessing the prototype directly. Or am I completely on the wrong track?

travelboy
  • 2,647
  • 3
  • 27
  • 37
  • Do you really need different classes? It seems they only differ in the value of some properties, so how about just one class `Animal` and a number of instances? – Thilo Sep 23 '11 at 02:49
  • I've also asked myself if I need different classes. It seems I do. I need more than just instances of Animal. I need instances of Elephants, instances of Zebras etc. All zebras share certain properties, all elephants share other properties. And all of them share a few basic animal properties. And I don't know these things at design time because God's creation is in a config file (which is only read at runtime). I found no satisfying way to model this with only one class. (And the real application is something different of course) :) – travelboy Sep 23 '11 at 03:21

2 Answers2

3

Since sound is a default value for an Animal instance, you can set it as a property on class definition:

class Cat extends Animal
    sound: 'meow!'

garfield = new Cat "garfield"
garfield.speak() # "garfield says meow!"

then

for animal in animalDefinitions
    animal.class = class extends Animal
        sound: animal.sound

mutant = new animalDefinitions[0].class "mutant"
mutant.speak() # "mutant says eek!"

If you want sound to be overridable, you can do

class Animal
    constructor: (@name, sound) ->
        @sound = sound if sound? 
    speak: ->
        console.log "#{@name} says #{@sound}"
Ricardo Tomasi
  • 34,573
  • 2
  • 55
  • 66
  • 1
    +1. Setting properties at class definition (instead of in the constructor) does not need the extra closure in the loop. Take a look at the generated JS to see the difference. – Thilo Sep 23 '11 at 03:27
  • Do you know if there´s any way to dynamically name the class or change the class name later on? `class animal.name extends Animal` names the class animal.name instead of the value of the variable... – thomasf1 Apr 09 '12 at 10:51
  • @thomasf1 to do that you need to make the class a property of some object, e.g. `animals[name] = class extends Animal` – Ricardo Tomasi Apr 09 '12 at 20:16
2

For your immediate problem (which is that the closure in the loop does not capture the current value, but the latest one), there is the do construct:

for animal in animalDefinitions
  do (animal) ->
    animal.class = class extends Animal
      constructor: (@name) ->
        @sound = animal.sound

I somehow expected CoffeeScript to take care of that automatically, since this is a common error in JavaScript, but at least with do there is a concise way to write it.

Thilo
  • 257,207
  • 101
  • 511
  • 656