Thursday, November 15, 2007

My Grails Aha Moment

Recently, I attended a training class led by Gruru Jeff Brown. (A Gruru is a Guru in Groovy & Grails, esp. if one is a committer)

I have some experience with Groovy and plenty experience with webapps, but none with Grails, nor with Ruby on Rails.

Here's a few neat tidbits that I discovered:

  • In terms of configuration, Grails has no XML and the only properties files are i18n messages. Almost everything else is Groovy. I am familiar with 'convention over configuration', but I didn't know the configuration was so fluid and dynamic.
  • Many of the language features of Groovy become perfectly obvious in the context of Grails. The big one, for me, was named parameters. As a Groovy language feature, I was neutral on it; in the DSL setting of a web application, the feature is vital. Similarly for closures and other constructions.
  • Also, the MOP (Meta-Object Protocol) becomes extremely important. However, it is so sublime that it is hard to "see". That is, the Groovy code looks so lucid that it is easy to gloss over the machinations behind the scenes.
The Moment

In Grails, we created a test application, and created a domain class.

Subsequently, we created an MVC controller for our app. At this point, we had a barebones app that could be launched and viewed.

Cool beans. However, the turning point for me was that Jeff said we could generate a controller: that is, Grails would generate the full code and then we could tweak it. Without a hint of irony or coyness, he warned us that it would generate a lot of source-code.

Being from the Java world, I braced myself for the output. Surely it would be verbose, impenetrable, and a rats nest in terms of formatting.

Surprise: the generated Groovy was gorgeous. Due to the brevity and style of the language, the generated code was 2 screens long -- without doc. And I was able to grok it and start tweaking it immediately.

At first, I just started changing some code. But then I realized that I was working with generated code. That was the profound moment. I understood.

Below, I've provided the generated code for a Gambler controller (think of a casino app) on my local machine. Give it a glance.... As a hint, the controller contains several methods which are actions (in MVC parlance) on either a target Gambler object or a list of Gambler objects.

Note that I'm not asking if you can understand it cold. I'm asking "do you think you could understand it, if warm?". Remember... this is generated.

Personally, I think it is stunning.

Final Score: Grails 1 My Old Biases 0. Check out Grails here.

class GamblerController {

def index = { redirect(action:list,params:params) }

// the delete, save and update actions only accept POST requests
def allowedMethods = [delete:'POST', save:'POST', update:'POST']

def list = {
if(!params.max) params.max = 10
[ gamblerList: Gambler.list( params ) ]
}

def show = {
[ gambler : Gambler.get( params.id ) ]
}

def delete = {
def gambler = Gambler.get( params.id )
if(gambler) {
gambler.delete()
flash.message = "Gambler ${params.id} deleted"
redirect(action:list)
}
else {
flash.message = "Gambler not found with id ${params.id}"
redirect(action:list)
}
}

def edit = {
def gambler = Gambler.get( params.id )

if(!gambler) {
flash.message = "Gambler not found with id ${params.id}"
redirect(action:list)
}
else {
return [ gambler : gambler ]
}
}

def update = {
def gambler = Gambler.get( params.id )
if(gambler) {
gambler.properties = params
if(!gambler.hasErrors() && gambler.save()) {
flash.message = "Gambler ${params.id} updated"
redirect(action:show,id:gambler.id)
}
else {
render(view:'edit',model:[gambler:gambler])
}
}
else {
flash.message = "Gambler not found with id ${params.id}"
redirect(action:edit,id:params.id)
}
}

def create = {
def gambler = new Gambler()
gambler.properties = params
return ['gambler':gambler]
}

def save = {
def gambler = new Gambler(params)
if(!gambler.hasErrors() && gambler.save()) {
flash.message = "Gambler ${gambler.id} created"
redirect(action:show,id:gambler.id)
}
else {
render(view:'create',model:[gambler:gambler])
}
}
}

No comments: