Archive for January 23rd, 2011

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.
Read Full Post | Make a Comment ( 25 so far )

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

Follow

Get every new post delivered to your Inbox.