Thursday, October 15, 2009

Over my last few Grails projects, although Grails cuts the number of code lines for a Web application down a lot, I realized that I was doing a lot more of code editing than I wanted to. A few areas on my projects needed some improvement: the controllers and the views (show, edit, create) are mostly the same for different domain objects and a lot of code for the persistence should be in services instead of the controllers.

I also wanted to be able to test the persistence layer independently from the controllers and being able to switch to something else than Hibernate if I wanted to without having to touch the controller code. So I designed a project with an "InstanceController", two views and an "InstanceService" that implements the common code for most domain objects. This is really the first draft and I envision a lot of improvements over this concept. With the dynamic features of Groovy, it could be possible to add the same features in a Groovy-er way. Work in progress.

The InstanceController works with two views for each domain object: "crud.gsp" and "list.gsp" and the InstanceService sub-classed for the particular domain object. So the structure of the Grails project is basically the same but "crud.gsp" implements all the functions of "edit.gsp", "show.gsp", "create.gsp".

So no more of editing three files when the domain object changes or the GUI changes. The GUI is consistent across the CRUD operations. The controller has an additional action "formactions" used in "crud.gsp" to manage the "cancel", "save", "edit" and "delete" buttons in the "crud.gsp" view. It also has a "void bindmore(instance, params)" method that can be overridden to bind checkboxes or other particular HTML elements to the domain class properties.

The usual "index", "list", "show", "create", "edit", "update", "delete" and "save" actions are still there if needed, but the view is handled by "crud.gsp" and the "formactions" in the controller. "formactions" calls "create", "edit", "save" and "delete" from the buttons clicked in "crud.gsp". All action closures call the methods of the same name to be able to override them in a subclass of the InstanceController.

The UI flow is: display "list.gsp", from an application menu for example, then a click on an item in the list calls "show" in the subclass of InstanceController for the domain object, which displays "view/domainobject/crud.gsp". Then clicking "delete", "edit", "save" or "cancel" calls the "formactions" closure in "/controller/domainobjectController". "formactions" calls "save", "update", "delete", "create" or displays the "list.gsp" view.

A couple of things there, the handling of "cancel" can be done with consistence, depending on the context and the application. "cancel" while editing means drop the changes and go back to the "show" mode, but "cancel" in show mode should go back to the "list" view. We could also hide the "cancel" button in "show" mode in "crud.gsp".

A small, but quite appreciated by the users, feature is highlighting the last object worked on in the "list.gsp" view. The object instance "id" is passed to "list.gsp" which adds the "selected" class to the line where the object is displayed, so the line is highlighted in the table (in yellow with my CSS style). It makes it very easy for the user to locate the last line worked on.

The GUI mode ("SHOW", "EDIT", "CREATE" is maintained between "crud.gsp" and "formactions" in the InstanceController and allows showing or hiding elements and buttons.

The subclass of the "InstanceController" must implement the "setup" method to define the "InstanceService" associated with the controller for the persistence operations and the domain object name displayed in the views. The "InstanceController" subclass can override any of the "InstanceController" methods if need be.

For the persistence layer, the "InstanceService" implements "list", "getCount", "getById", "newinstance", "save", "update", "delete", "findByName", "createDefaults" (creates initial values if none exist in the database) and "archive" if the domain object has an "archived" property. The subclass of "InstanceService" for the domain object needs to implement the "setup" method, where the initial domain objects are defined in the "defaults" list if needed. The "instanceclassname" containing the domain class name must be defined as well as the domain object property name used for sorting the "list.gsp" view. Here too, the "InstanceService" subclass can override any of the "InstanceService" methods if need be. Last thing not to forget, the "setup" method of each "InstanceService" subclass must be called in "Bootstrap.groovy", or in the "setUp" method of any integration test.

The domain class must implement "String toMessage()", to display a user-friendly domain object name. For example, my class name is "Streettype", but "toMessage" will return "Street Type". It's used in "crud.gsp" and "list.gsp". "toString" could be used, but then a bit tricky if "toString" is also used for debugging. I guess we could check if "toMessage" is implemented and use "toString" if not.

The code is posted at http://code.google.com/p/thecodefactory/wiki/GrailsRefactoring