A Pattern to Simplify Grails Controllers

Posted on January 23, 2011. Filed under: grails |

The WithDomainObject Pattern

Note, the sample code was modified on 5/18/2011. The withPerson method is now private.

I generally follow the same patterns with my controllers. They start with “the big 7 actions” – closures for index, list, show, create, save, edit, update and delete. Then i include any necessary controller-specific actions. After unit testing my controllers for the up-teenth time, I realized that there is a consistent pattern for many of the actions – get the id from params, get the domain object, then use the domain object. I wanted to extract this pattern into its own method, and that extraction evolved into the “WithDomainObject” pattern.

The Problem

Lets say you had a contact manager application, and you had a domain object called Person and a controller called PersonController. In the controllers show, save, edit, update and delete actions all follow the same pattern:

  1. Get the ID from the params map.
  2. Get the person from the domain.
  3. Doing something with the person.

The problem is that steps 1 and 2 are repeated for the 5 actions. Not very DRY.

The Solution

Lets encapsulate steps 1 and 2 into a method called withPerson:

	private def withPerson(id="id", Closure c) {
		def person = Person.get(params[id])
		if(person) {
			c.call person
		} else {
			flash.message = "The person was not found."
			redirect action:"list"
		}
	}
}

and lets put our code from step 3 into its own anonymous closure. Heres a example of a action that uses the withPerson method and the update action:

	def update = {
		withPerson { person ->
			person.properties = params
			if(person.validate() && person.save()) {
				redirect action:"show", id:person.id
			} else {
				render view:"edit", model:[person:person]
			}
		}
	}

Example

See the withPerson method at the botttom of the controller.

The Person domain class


package contact

class Person {

	String name

        static constraints = {
    	    name blank:false, unique:true
    }
}

The PersonController controller


package contact

class PersonController {

	def index = {
		redirect action:"list", params:params
	}

	def list = {
		[ people:Person.list(params), count:Person.count() ]
	}

	def show = {
		withPerson { person ->
			[person:person]
		}
	}

	def create = {
		[person:new Person()]
	}

	def save = {
		def person = new Person(params)
		if(person.validate() && person.save()) {
			redirect action:"show", id:person.id
		} else {
			render view:"create", model:[person:person]
		}
	}

	def edit = {
		withPerson { person ->
			[person:person]
		}
	}

	def update = {
		withPerson { person ->
			person.properties = params
			if(person.validate() && person.save()) {
				redirect action:"show", id:person.id
			} else {
				render view:"edit", model:[person:person]
			}
		}
	}

	def delete = {
		withPerson { person ->
			person.delete()
			redirect action:"list"
		}
	}

	private def withPerson(id="id", Closure c) {
		def person = Person.get(params[id])
		if(person) {
			c.call person
		} else {
			flash.message = "The person was not found."
			redirect action:"list"
		}
	}

}

The PersonControllerTests unit tests


package contact

import grails.test.*
import org.junit.*

class PersonControllerTests extends ControllerUnitTestCase {

    def p7 = new Person(id:7, name:"alpha")
    def p9 = new Person(id:9, name:"beta")

    @Before
    public void setUp() {
        super.setUp()
    	mockDomain Person, [p7,p9]
    }

    @After
    public void tearDown() {
        super.tearDown()
    }

    @Test
    public void index() {
		controller.index()
		assert "list" == controller.redirectArgs.action
    }

    @Test
    public void list() {
    	def model = controller.list()
    	assert 2 == model.people.size()
    	assert p7 == model.people[0]
    	assert p9 == model.people[1]
    	assert 2 == model.count
    }

    @Test
    public void show() {
    	controller.params.id = 7
    	def model = controller.show()
    	assert p7 == model.person
    }

    @Test
    public void create() {
    	def model = controller.create()
    	assert model.person instanceof Person
    }

    @Test
    public void save_success() {
    	controller.params.name = "Paul Woods"
    	controller.save()
    	assert "show" == controller.redirectArgs.action
    	assert null != controller.redirectArgs.id
    }

    @Test
    public void save_failure() {
    	controller.params.name = ""
    	controller.save()
    	assert "create" == controller.renderArgs.view
    	assert controller.renderArgs.model.person instanceof Person
    }

    @Test
    public void edit() {
    	controller.params.id = 9
    	def model = controller.edit()
    	assert p9 == model.person
    }

    @Test
    public void update_success() {
    	controller.params.name = "Paul Woods"
    	controller.params.id = 7
    	controller.update()
    	assert "show" == controller.redirectArgs.action
    	assert 7 == controller.redirectArgs.id
    }

    @Test
    public void update_failure() {
    	controller.params.name = ""
    	controller.params.id = 9
    	controller.update()
    	assert "edit" == controller.renderArgs.view
    	assert controller.renderArgs.model.person instanceof Person
    }

    @Test
    public void delete() {
    	controller.params.id = 7
    	controller.delete()
    	assert "list" == controller.redirectArgs.action
    	assert 1 == Person.count()
    }

    @Test
    public void withPerson_success() {
    	controller.params.id = 7
    	def person = null
    	controller.withPerson() { p ->
    		person = p
    	}

    	assert 7 == person.id
    }

    @Test
    public void withPerson_fail() {
    	controller.params.id = 0
    	controller.withPerson() { p ->
    		assert false
    	}
	assert "The person was not found." == controller.flash.message
	assert "list" == controller.redirectArgs.action
   }

}

Advantages of this Pattern:

  1. The controller actions are simpler, as they don’t require code to detect not-found domain objects.
  2. The unit tests can be simpler. There is less code to test.
About these ads

25 Responses to “A Pattern to Simplify Grails Controllers”

RSS Feed for Mr Paul Woods's Weblog Comments RSS Feed

This is very cool and could actually make a very useful plugin.

Putting in a plugin would mean that instead of adding the method manually, you could write a generic withDomain{} method that resolves domain names via GrailsNameUtils.

Interesting idea. It would work great when the Domains match up with the controllers (eg: Person and PersonController, EMail and EMailController), but how should it work if the controller doesn’t match a domain?

Thanks
Paul Woods

In the default scaffolding, you can specify the domain class to scaffold against. I would imagine setting a variable at the controller level. static useDomain = ‘car’.

Additionally, withDomain should also take an argument, i.e.

withDomain( ‘car’ ){ car code here }

I see. I like your idea. I may attempt it over the weekend.

Thanks,
Paul Woods

In rails the common patterns for this seems to be before_filters. i.e, define a finder method the domain object and call it using before for relevant actions. Wouldn’t the same apply for grails?

Twice, maybe – I know just a little about the details of Rails.

In Grails you have to manually fetch your domain object in each controller action, and therefore you have to handle any error cases involving not finding that domain object.

My code adds a method to do the fetching and handle errors.

The screencasts on Rails that I have seen all show a 1:1 correlation between Domain Objects and Controllers. How does it work in Rails when a controller manages multiple domain objects?

Thanks,
Paul

Pretty cool idea. Could be really helpful ‘out of the box’ if generated by Grails, but I guess the other comments might prevent that – about what if the ‘convention’ was not followed between domain object and controller.

Nice idea.

You could make the withPerson work like this:

withPerson{
properties = params
if(validate)…

}

by changing withPerson() to something like:

def withPerson(id=”id”, Closure c) {
def person = Person.get(params[id])
if(person) {
c.delegate = person
c.resolveStrategy = closure.DELEGATE_FIRST
c.call
} else {
flash.message = “The person was not found.”
redirect action:”list”
}

Peter

How would this work:

def show = {
withPerson { person ->
[person:person]
}
}

How do you get the person object, if its the delegate?

Paul

Good question… try

def person = withPerson {
… do stuff
}
[person: person]

and return person at the end of withPerson… kinda spoils it I know ;-/

this works too:

withPerson {
hairColour = ‘blond’
render(view: “changeColour”, model: [person: delegate])
}

[…] A Pattern to Simplify Grails Controllers « Mr Paul Woods’s Weblog […]

Peter, While I like your code sample and it works well, it is not my style. I prefer explicitly defined variable instead of using delegate. For me it is easier to understand and easier to teach.

Thanks,
Paul Woods

[…] A Pattern to Simplify Grails Controllers « Mr Paul Woods’s Weblog – Grails und Coding by Convention, Großartiges Beispiel wie es noch mehr "Jedes-Mal-Aufs-Neue-Arbeit" zusammengefasst und eingespart wird […]

Very nice. I tried doing this in my Controller template (src/templates/scaffolding/Controller.groovy) and it worked with one minor fix. The withPerson method (with${className} in the template) can’t access the controller params object directly. However, a it’s possible to get to it through the passed in closure. Replacing params[id] with c.owner.params[id] fixed it.

Andrew

Interesting. I didn’t think about editing the scaffolding controller.groovy file. I’ll remember that trick.

Thanks, Paul

Love it. Am going to implement this immediately.

Ugh… know of one reason why not to implement this (at least not as a closure); ACL’s. You’d probably want to lock this down securely to avoid people grabbing variables or data from your domains when they may not have privileges. This is basically a backdoor (unless I’m misunderstanding something).

Now if you rewrite this as a private function, this will work.

Owen, you are correct. the method should be private. I will update the example code.

Thanks,
Paul Woods

Thanks a great idea. Thanks!

Just saw this now. Great idea!

Reblogged this on Oele Geirnaert and commented:
Usefull information i think!

The pattern is really great. Is there already a plugin for it or does anybody have a scaffolding example?

Thanks
flotsch

Thanks, flotsch. I have not made a plugin for it (and most likely will not). I don’t think anyone else has either.

Paul


Comments are closed.

Liked it here?
Why not try sites on the blogroll...

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: