grails patterns and practices
Post on 18-Oct-2014
369 views
DESCRIPTION
Slides from Grails coding Dodo (OpenCredo 2011)TRANSCRIPT
Grails: Patterns & Practices
Paul BowlerSenior 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 in Views?)
You did create some tests first, right?
Implementation
class Task {Inventory inventoryString description
static belongsTo = [Inventory]
static constraints = {description(nullable: false, blank: false)
}}
class Inventory {String namestatic hasMany = [tasks: Task]
static constraints = {name(nullable: false, blank: false, unique: true)
}}
Domain Testsclass 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 inventoryString 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
Page View Page ViewPage View
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 Flowclass 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 Negotiationclass 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.tasksrender(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.gspgrails-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.