KEMBAR78
Grails patterns and practices | PDF
Grails: Patterns &
Practices
Paul Bowler
Senior Consultant, OpenCredo
Who are you?
Coding Dojo
• Agile!
• Teams of 2-4 people
• 1 sprint = 20 minutes
• Domain-Drive Design (I’m the Product Owner)
• Test-Driven Development (Maybe!)
• Yes, you can use the user guide and internet!
• User demo at the end of each sprint
• Discussion + Refactoring
• Prize for best app!
?
5 ‘Maturity’ Levels
?
?
?
?
Domain-Driven Design
• “Domain-driven design (DDD) is an approach to developing
software for complex needs by deeply connecting the
implementation to an evolving model of the core business
concepts.”
• The premise of domain-driven design is the following:
• Placing the project's primary focus on the core domain and
domain logic
• Basing complex designs on a model
• Initiating a creative collaboration between technical and
domain experts to iteratively cut ever closer to the
conceptual heart of the problem.
User Story 1
“As a pomodoro fan, I would like to be able to add
tasks to a uniquely named activity inventory, so that I
can see what work I need to complete over the next
few weeks.”
Useful Commands
• grails create-app pomodoro
• grails create-domain-class <domain>
• grails generate-all <domain>
• grails generate-controller <domain> and
add ‘static scaffold = true’ to controller
Considerations
• Associations
• One-to-One
• One-to-Many
• Many-to-Many
• Constraints &Validation
• Time-Stamping?
• Default values (and field values inViews?)
You did create some
tests first, right?
Implementation
class Task {
Inventory inventory
String description
static belongsTo = [Inventory]
static constraints = {
description(nullable: false, blank: false)
}
}
class Inventory {
String name
static hasMany = [tasks: Task]
static constraints = {
name(nullable: false, blank: false, unique: true)
}
}
Domain Tests
class InventoryTests extends GrailsUnitTestCase {
void testConstraints() {
def existingInventory = new Inventory(name: "Paul’s Inventory")
mockForConstraintsTests(Inventory, [ existingInventory ])
! ! // Validation should fail if both properties are null.
! ! def inventory = new Inventory()
! ! assertFalse inventory.validate()
! ! assertEquals "nullable", inventory.errors["description"]
! ! // So let's demonstrate the unique constraint.
! ! inventory = new Inventory(name: "Paul’s Inventory")
! ! assertFalse inventory.validate()
! ! assertEquals "unique", inventory.errors["name"]
! ! // Validation should pass!
! ! inventory = new Inventory(name: "John’s Inventory")
! ! assertTrue inventory.validate()
! }
}
Gotcha!
• Potential performance issue with mapped
collections:
• Adding to the Set requires loading all
instances from the database to ensure
uniqueness
• Likewise for mapped List
• Works fine in development, but what if
you have 1,000,000+ rows?
Implementation (2)
class Task {
Inventory inventory
String description
static constraints = {
description(nullable: false, blank: false)
}
}
class Inventory {
String name
}
Side-effects?
• Different syntax for adding Tasks
• No cascading deletes
• Custom finder required to find all Tasks in
an Inventory
• Scaffolding breaks!
User Story 2
“As a pomodoro fan, I would like to be able move tasks
onto a ‘To Do Today’ sheet, so that I can see work to be
completed today and view my work history.”
Considerations
• Does the ‘Today’ list share any common
attributes with the Inventory?
• How about a more intuitive URL scheme?
?
Level 1
?
?
?
Views
Level 1 -Views
Controller
Model
PageView PageViewPageView
Model Model
Level 1 ‘Smells’
• Logic built into pages:
• Overuse of Request Parameters
• If-Then tags
• Inline groovy using ${...}
• Poor use of layouts
• Little use of tags
• Domain classes as simple ‘active records’
• Page-based information architecture
?
Level 2
?
?
Controllers
Views
Level 1-2 Refactoring
• Move logic out of pages into controllers
• Reduce pages into fragments
• Use layouts to construct device or stakeholder-
centric views from pages and fragments
• Use available tag libraries
• Create your own tag libraries!
• Stylesheets rule - minimise markup
User Story 3
“As a pomodoro fan, I would like to have an optimised
workflow for US2, so that I can save time and reduce
input mistakes.”
Considerations
• Web Flow plugin?
• Command Objects?
• What changes need to be made to domain
classes?
Web Flow
class InventoryController {
…
def inventoryFlow = {
showInventory {
on("done").to "saveInventory"
on("continue").to "addTask"
}
…
addTask {
redirect(controller:"task", action:"create")
}
saveInventory()
}
}
<g:form action="inventory">
<g:submitButton name="continue" value="Add Another"></g:submitButton>
<g:submitButton name="done" value="I’m Done"></g:submitButton>
</g:form>
User Story 4
“As a pomodoro fan, I would like to be able update the
number of iterations I’ve completed on each task in my
‘To Do Today’ list, so that I can keep track of my
progress and improve my future estimates.”
Considerations
• Can we do this without page refreshes?
• How can we test this?
• Domain changes?
Level 2 - Controllers
Controller
Domain
Controller Controller
Domain Domain Domain
Layout
Fragments Fragments
Layout
Fragments Fragments
Layout
Fragments Fragments
Level 2 ‘Smells’
• Large, complex Controllers
• Different scenarios driven by ‘If/Then’ logic
• Content negotiation increases complexity
further
• Many similar controller methods (not
DRY!)
• Poorly handled Transactions
?
Level 3 - Services
?
Services
Controllers
Views
Level 2-3 Refactoring
• Move domain transaction logic out of
controllers into services
• Controllers should be ‘glue’ that binds
business services to UI
• Service methods should reflect business
scenarios
• Make use of transactional capability of
services
User Story 5
“As a pomodoro partner, I would like a simple REST
API over your daily task view, so I can integrate your
data into my application.”
Considerations
• Don’t clutter your Controllers!
• REST-ful URLs
• Content negotiation?
• Custom XML/JSON formats?
RESTful URL Mappings
static mappings = {
"/task/$id?"(resource:"task")
}
static mappings = {
"/task/$id"(controller:"task") {
action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
}
}
static mappings = {
"/task/$id"(controller:"task", parseRequest:true) {
action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
}
}
Content Negotiation
class InventoryController {
def inventory
def list = {
this.inventory = Inventory.list()
withFormat {
html inventoryList:inventory
json { render inventory as JSON }
xml { render inventory as XML }
}
}
}
Custom Formats?
def listAsXML = {
def inventory = Inventory.get(params.id)
def tasks = inventory.tasks
render(contentType:"text/xml") {
inventory(name:inventory.name) {
tasks {
for(t in tasks) {
task(title:t.title)
! }
}!
}
}
}
User Story 6
“As a pomodoro fan, I’d like to be able to add
unplanned and urgent tasks to the bottom of my daily
list, so that I can track and manage interruptions.”
Level 3 - Services
Controller
Domain
Controller Controller
Domain Domain Domain
Services
Layout
Fragments Fragments
Layout
Fragments Fragments
Layout
Fragments Fragments
Level 3 ‘Smells’
• Large, complex Services
• Services acting as proxies for domain
behaviour
• ‘Cut-and-paste’ methods
?
Level 4 - Libraries
Libraries
Services
Controllers
Views
Level 3-4 Refactoring
• Move common code out of services into
POGO’s (or POJO’s)
• Enrich our domain model to simplify services:
• Named Queries
• Derived Properties
• Criteria: Conjunctions, Disjunctions,
Projections, Restrictions
User Story 7
“As a pomodoro fan, I would like to be able to search
for tasks on my inventory through a simple interface, so
I can find and modify them easily.”
Level 4 - Libraries
Controller
Domain
Controller Controller
Domain Domain Domain
Services
Libraries Libraries
Layout
Fragments Fragments
Layout
Fragments Fragments
Layout
Fragments Fragments
Level 4 ‘Smells’
• Large, monolithic application
• Increased cognitive overhead
• New starters struggle
• Components ‘cut and pasted’ into similar
projects
Plugins
Level 5 - Plugins
Libraries
Services
Controllers
Views
Level 4-5 Refactoring
• Componentise the application into plugins
• Construct applications by combining plugins
• Could your application itself be constructed
as a plugin for an organisation’s product
suite?
• Writing plugins that modify the Grails/Spring
context is beyond the scope of this
workshop!
User Story 8
“As a pomodoro fan, I would like a simplified version of
my Inventory, so I can view it on my iPhone.”
Useful Commands
• grails create-plugin <plugin>
• grails package-plugin
• grails install-plugin /path/to/plugin/grails-
example-0.1.zip
Layouts and Fragments
<g:include action="show" id="1" />
<g:include action="show" id="${currentTask.id}" />
<g:include controller="task" />
<g:include controller="task" action="list" />
<g:include action="list" params="[sort:'title', order:'asc'] />
<html>
<head>
<title><g:layoutTitle default="An example decorator" /></title>
<g:layoutHead />
</head>
<body>
<div class="menu"><!--my common menu goes here--></menu>
<div class="body">
<g:layoutBody />
</div>
</div>
</body>
</html>
Layout Options
• In your views:
<meta name="layout" content="main"></meta>
• In your controller:
static layout = 'task'
static layout = 'custom/task'
• By Convention:
grails-app/views/layouts/task.gsp
grails-app/views/layouts/task/list.gsp
• Inline:
<g:applyLayout name="myLayout" template="taskTemplate" collection="${tasks}" />
<g:applyLayout name="myLayout" url="http://www.google.com" />
<g:applyLayout name="myLayout">The content to apply a layout to</g:applyLayout>
Level 5 - Plugins
Controller
Domain
Controller
Domain Domain
Plugins
Page
View
Page
View
Controller
Services
Libraries / Plugins
Domain Domain
Layout
Fragments Fragments
Layout
Fragments Fragments
Services
Libraries Libraries
Plugins
Libraries
Services
Controllers
The Full Picture
Views
Phew!
Well Done.

Grails patterns and practices

  • 1.
    Grails: Patterns & Practices PaulBowler Senior Consultant, OpenCredo
  • 2.
  • 3.
    Coding Dojo • Agile! •Teams of 2-4 people • 1 sprint = 20 minutes • Domain-Drive Design (I’m the Product Owner) • Test-Driven Development (Maybe!) • Yes, you can use the user guide and internet! • User demo at the end of each sprint • Discussion + Refactoring • Prize for best app!
  • 4.
  • 5.
    Domain-Driven Design • “Domain-drivendesign (DDD) is an approach to developing software for complex needs by deeply connecting the implementation to an evolving model of the core business concepts.” • The premise of domain-driven design is the following: • Placing the project's primary focus on the core domain and domain logic • Basing complex designs on a model • Initiating a creative collaboration between technical and domain experts to iteratively cut ever closer to the conceptual heart of the problem.
  • 7.
    User Story 1 “Asa pomodoro fan, I would like to be able to add tasks to a uniquely named activity inventory, so that I can see what work I need to complete over the next few weeks.”
  • 8.
    Useful Commands • grailscreate-app pomodoro • grails create-domain-class <domain> • grails generate-all <domain> • grails generate-controller <domain> and add ‘static scaffold = true’ to controller
  • 9.
    Considerations • Associations • One-to-One •One-to-Many • Many-to-Many • Constraints &Validation • Time-Stamping? • Default values (and field values inViews?)
  • 10.
    You did createsome tests first, right?
  • 11.
    Implementation class Task { Inventoryinventory String description static belongsTo = [Inventory] static constraints = { description(nullable: false, blank: false) } } class Inventory { String name static hasMany = [tasks: Task] static constraints = { name(nullable: false, blank: false, unique: true) } }
  • 12.
    Domain Tests class InventoryTestsextends GrailsUnitTestCase { void testConstraints() { def existingInventory = new Inventory(name: "Paul’s Inventory") mockForConstraintsTests(Inventory, [ existingInventory ]) ! ! // Validation should fail if both properties are null. ! ! def inventory = new Inventory() ! ! assertFalse inventory.validate() ! ! assertEquals "nullable", inventory.errors["description"] ! ! // So let's demonstrate the unique constraint. ! ! inventory = new Inventory(name: "Paul’s Inventory") ! ! assertFalse inventory.validate() ! ! assertEquals "unique", inventory.errors["name"] ! ! // Validation should pass! ! ! inventory = new Inventory(name: "John’s Inventory") ! ! assertTrue inventory.validate() ! } }
  • 13.
    Gotcha! • Potential performanceissue with mapped collections: • Adding to the Set requires loading all instances from the database to ensure uniqueness • Likewise for mapped List • Works fine in development, but what if you have 1,000,000+ rows?
  • 14.
    Implementation (2) class Task{ Inventory inventory String description static constraints = { description(nullable: false, blank: false) } } class Inventory { String name }
  • 15.
    Side-effects? • Different syntaxfor adding Tasks • No cascading deletes • Custom finder required to find all Tasks in an Inventory • Scaffolding breaks!
  • 16.
    User Story 2 “Asa pomodoro fan, I would like to be able move tasks onto a ‘To Do Today’ sheet, so that I can see work to be completed today and view my work history.”
  • 17.
    Considerations • Does the‘Today’ list share any common attributes with the Inventory? • How about a more intuitive URL scheme?
  • 18.
  • 19.
    Level 1 -Views Controller Model PageViewPageViewPageView Model Model
  • 20.
    Level 1 ‘Smells’ •Logic built into pages: • Overuse of Request Parameters • If-Then tags • Inline groovy using ${...} • Poor use of layouts • Little use of tags • Domain classes as simple ‘active records’ • Page-based information architecture
  • 21.
  • 22.
    Level 1-2 Refactoring •Move logic out of pages into controllers • Reduce pages into fragments • Use layouts to construct device or stakeholder- centric views from pages and fragments • Use available tag libraries • Create your own tag libraries! • Stylesheets rule - minimise markup
  • 23.
    User Story 3 “Asa pomodoro fan, I would like to have an optimised workflow for US2, so that I can save time and reduce input mistakes.”
  • 24.
    Considerations • Web Flowplugin? • Command Objects? • What changes need to be made to domain classes?
  • 25.
    Web Flow class InventoryController{ … def inventoryFlow = { showInventory { on("done").to "saveInventory" on("continue").to "addTask" } … addTask { redirect(controller:"task", action:"create") } saveInventory() } } <g:form action="inventory"> <g:submitButton name="continue" value="Add Another"></g:submitButton> <g:submitButton name="done" value="I’m Done"></g:submitButton> </g:form>
  • 26.
    User Story 4 “Asa pomodoro fan, I would like to be able update the number of iterations I’ve completed on each task in my ‘To Do Today’ list, so that I can keep track of my progress and improve my future estimates.”
  • 27.
    Considerations • Can wedo this without page refreshes? • How can we test this? • Domain changes?
  • 28.
    Level 2 -Controllers Controller Domain Controller Controller Domain Domain Domain Layout Fragments Fragments Layout Fragments Fragments Layout Fragments Fragments
  • 29.
    Level 2 ‘Smells’ •Large, complex Controllers • Different scenarios driven by ‘If/Then’ logic • Content negotiation increases complexity further • Many similar controller methods (not DRY!) • Poorly handled Transactions
  • 30.
    ? Level 3 -Services ? Services Controllers Views
  • 31.
    Level 2-3 Refactoring •Move domain transaction logic out of controllers into services • Controllers should be ‘glue’ that binds business services to UI • Service methods should reflect business scenarios • Make use of transactional capability of services
  • 32.
    User Story 5 “Asa pomodoro partner, I would like a simple REST API over your daily task view, so I can integrate your data into my application.”
  • 33.
    Considerations • Don’t clutteryour Controllers! • REST-ful URLs • Content negotiation? • Custom XML/JSON formats?
  • 34.
    RESTful URL Mappings staticmappings = { "/task/$id?"(resource:"task") } static mappings = { "/task/$id"(controller:"task") { action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"] } } static mappings = { "/task/$id"(controller:"task", parseRequest:true) { action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"] } }
  • 35.
    Content Negotiation class InventoryController{ def inventory def list = { this.inventory = Inventory.list() withFormat { html inventoryList:inventory json { render inventory as JSON } xml { render inventory as XML } } } }
  • 36.
    Custom Formats? def listAsXML= { def inventory = Inventory.get(params.id) def tasks = inventory.tasks render(contentType:"text/xml") { inventory(name:inventory.name) { tasks { for(t in tasks) { task(title:t.title) ! } }! } } }
  • 37.
    User Story 6 “Asa pomodoro fan, I’d like to be able to add unplanned and urgent tasks to the bottom of my daily list, so that I can track and manage interruptions.”
  • 38.
    Level 3 -Services Controller Domain Controller Controller Domain Domain Domain Services Layout Fragments Fragments Layout Fragments Fragments Layout Fragments Fragments
  • 39.
    Level 3 ‘Smells’ •Large, complex Services • Services acting as proxies for domain behaviour • ‘Cut-and-paste’ methods
  • 40.
    ? Level 4 -Libraries Libraries Services Controllers Views
  • 41.
    Level 3-4 Refactoring •Move common code out of services into POGO’s (or POJO’s) • Enrich our domain model to simplify services: • Named Queries • Derived Properties • Criteria: Conjunctions, Disjunctions, Projections, Restrictions
  • 42.
    User Story 7 “Asa pomodoro fan, I would like to be able to search for tasks on my inventory through a simple interface, so I can find and modify them easily.”
  • 43.
    Level 4 -Libraries Controller Domain Controller Controller Domain Domain Domain Services Libraries Libraries Layout Fragments Fragments Layout Fragments Fragments Layout Fragments Fragments
  • 44.
    Level 4 ‘Smells’ •Large, monolithic application • Increased cognitive overhead • New starters struggle • Components ‘cut and pasted’ into similar projects
  • 45.
    Plugins Level 5 -Plugins Libraries Services Controllers Views
  • 46.
    Level 4-5 Refactoring •Componentise the application into plugins • Construct applications by combining plugins • Could your application itself be constructed as a plugin for an organisation’s product suite? • Writing plugins that modify the Grails/Spring context is beyond the scope of this workshop!
  • 47.
    User Story 8 “Asa pomodoro fan, I would like a simplified version of my Inventory, so I can view it on my iPhone.”
  • 48.
    Useful Commands • grailscreate-plugin <plugin> • grails package-plugin • grails install-plugin /path/to/plugin/grails- example-0.1.zip
  • 49.
    Layouts and Fragments <g:includeaction="show" id="1" /> <g:include action="show" id="${currentTask.id}" /> <g:include controller="task" /> <g:include controller="task" action="list" /> <g:include action="list" params="[sort:'title', order:'asc'] /> <html> <head> <title><g:layoutTitle default="An example decorator" /></title> <g:layoutHead /> </head> <body> <div class="menu"><!--my common menu goes here--></menu> <div class="body"> <g:layoutBody /> </div> </div> </body> </html>
  • 50.
    Layout Options • Inyour views: <meta name="layout" content="main"></meta> • In your controller: static layout = 'task' static layout = 'custom/task' • By Convention: grails-app/views/layouts/task.gsp grails-app/views/layouts/task/list.gsp • Inline: <g:applyLayout name="myLayout" template="taskTemplate" collection="${tasks}" /> <g:applyLayout name="myLayout" url="http://www.google.com" /> <g:applyLayout name="myLayout">The content to apply a layout to</g:applyLayout>
  • 51.
    Level 5 -Plugins Controller Domain Controller Domain Domain Plugins Page View Page View Controller Services Libraries / Plugins Domain Domain Layout Fragments Fragments Layout Fragments Fragments Services Libraries Libraries
  • 52.
  • 53.