I'm trying to get the usage of Command Objects right. And I've got some questions.
Given a generic domain, for instance:
package com.foo
class Ticket {
Customer customer
Product product
Defect defect
Solution solution
String comment
static constraints = {
customer nullable:false
product nullable:false
defect nullable:true
solution nullable:true
comment nullable:true
}
}
Then, consider the following rules of the Ticket's form:
- Customer can only be selected on creating. On editing, the select must not be shown, displaying a label instead;
- Product can only be selected on creating. Yet, on editing, the select must be shown disabled;
- Defect can be selected either on creating or editing;
- Solution cannot be set in this form whatsoever.
- Comment can only be informed by users with a certain role (using SpringSecurity, for example). If the user does not have such a role, the text area must be shown disabled.
Now, what I wanna know is:
What would be the best approach to using CommandObject to handle this scenario?
- 1 single CommandObject for both actions?
- 1 CommandObject specific for each action?
- In the case of a single CommandObject, how to prevent users from hacking the program, passing a forbidden parameter, for instance?
What would be the best approach to implementing the form's rules? That is, which field is shown/enabled/disabled in each case.
- Is there any pattern or recommendation for this case?
- Should such rules be written in the actual form?
- Or should the form "ask" somebody? The CommandObject perhaps? Or the Domain instance itself?
For example, consider this form's gist:
<div class="fieldcontain ${hasErrors(bean:ticketInstance, field:'customer', 'error')} required">
<label for="customer">
<g:message code="ticket.customer.label" default="Customer" />
<span class="required-indicator">*</span>
</label>
<g:if test="${ticketInstance.id}">
<span class="label read-only">${ticketInstance.customer.name}</span>
</g:if>
<g:else>
<g:select id="customer" name="customer.id" from="${Customer.list()}" optionKey="id"
required="" disabled="" value="${ticketInstance?.customer?.id}" class="many-to-one"/>
</g:else>
</div>
In this case, there is no much a problem, because the check is rather simple. That is:
<g:if test="${ticketInstance.id}">
...
However, consider a more complex rule. Something like:
<g:if test="${ticketInstance.id && currentUser.granted('SOME_RULE') && ticketInstance.someField != null}">
...
And so on.
Now, there are a few problems with this approach:
- It is rather verbose, and hence prone to mistakes.
- Suppose that such a rule was shared among other fields. In this case, I would have to manage it somehow (a local variable, duplicate the code, and etc).
- And plus, in order to this, I would need a property 'Id' in my TicketCommand, which I don't know if it is a good idea.
And thus, I was wondering if there is any pattern or recommendation that can be used to improve these scenarios. That is, something that would encapsulate such a complexity. For instance:
<g:if test="${cmd.customerAllowed}">
...
Where the CommandObject could be something along the lines:
@Validateable
class TicketCreateCommand {
def currentUser //injected somehow..
Customer customer
Product product
Defect defect
String comment
static constraints = {
importFrom Ticket
}
boolean isNew() {
true
}
boolean isCustomerAllowed() {
this.new && currentUser.granted('SOME_RULE') && this.someField != null
//some more rules if necessary..
}
boolean isSomeFieldAllowed() {
//rules for creating
}
}
And a CommandObject for editing:
@Validateable
class TicketEditCommand {
def currentUser //injected somehow..
Customer customer
Product product
Defect defect
String comment
static constraints = {
importFrom Ticket
}
boolean isNew() {
false
}
boolean isCustomerAllowed() {
this.new && currentUser.granted('SOME_RULE') && this.someField != null
//some more rules if necessary..
}
boolean isSomeFieldAllowed() {
//rules for editing
}
}
Can a CommndObject hold such responsibilities? If not, is there any other better way to centralise such complexities? Plus, as I've said before, the property customer cannot be set on updating. How to deal with this?
Well, I reckon that this is pretty much it.
I'd appreciate any opinions and suggestions. Any link for some tutorial would be brilliant as well.
PS: For those who wanna have a look, the full project is available on github.