In simple terms, a Specification is a small encapsulated unit of logic that gives an answer to a simple straight forward question: “does it match?”
Using a Specification we will split the logic of how we make a selection, away from objects we are selecting.
The Specification Pattern was originally proposed by Martin Fowler and Eric Evans.
Here is Dojo implementation of the CompositeSpecification.
/* * The Specification Pattern JavaScript Implementation * * A specification is a powerful design pattern able to tell if a candidate object * matches some criteria. The specification has a method isSatisfiedBy(entity) * that returns true if all criteria are met by entity * * @example http://www.martinfowler.com/apsupp/spec.pdf * @example http://en.wikipedia.org/wiki/Specification_pattern * @example http://devlicio.us/blogs/jeff_perrin/archive/2006/12/13/the-specification-pattern.aspx * @example http://devlicio.us/blogs/casey/archive/2009/03/02/ddd-the-specification-pattern.aspx * * * LICENSE: Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit * persons to whom the Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @author Goran Miskovic, schkovich[at]gmail[dot]com * @copyright 2010 Goran Miskovic © * @license http://www.opensource.org/licenses/mit-license.php The MIT License * @version SVN: $Id: CompositeSpecification.js 85 2011-03-02 22:26:46Z schkovich $ */ dojo.provide("schkovich.CompositeSpecification"); dojo.declare("schkovich.CompositeSpecification", null, { constructor: function() { var AndSpecification = dojo.declare(schkovich.CompositeSpecification, { constructor: function(self, other) { var One = self; var Other = other; this.IsSatisfiedBy = function(entity) { return One.IsSatisfiedBy(entity) && Other.IsSatisfiedBy(entity); } } }); var OrSpecification = dojo.declare(schkovich.CompositeSpecification, { constructor: function(self, other) { var One = self; var Other = other; this.IsSatisfiedBy = function(entity) { return One.IsSatisfiedBy(entity) || Other.IsSatisfiedBy(entity); } } }); var NotSpecification = dojo.declare(schkovich.CompositeSpecification, { constructor: function(self) { var Wrapped = self; this.IsSatisfiedBy = function(entity) { return !Wrapped.IsSatisfiedBy(entity); } } }); this.And = function(other) { return new AndSpecification(this, other); } this.Or = function(other) { return new OrSpecification(this, other); } this.Not = function() { return new NotSpecification(this); } } })
We will setup very simple example. Imagine you are librarian and the students asks for a list of composers that where productive during Romantic or Post-Romantic epoch and have compositions in C-minor or G-major.
In order to simplify the example we will assume that those whose first name starts with letter C did compose something in C-minor and that those whose first name starts on G composed something in G-major. :)
Data are structured in this way:
{"id":"1","firstName":"Johann Sebastian","lastName":"Bach","category":"Baroque"}First we will need to create epoch specification:
var EpochSpecification = dojo.declare( schkovich.CompositeSpecification, { constructor: function(epoch) { this.IsSatisfiedBy = function(entity) { return entity.category && (entity.category === epoch); } } });
Then we need logic to select ones having compositions in C-minor or G-major or as we assumed ones whose first name starts with letter C or G:
var FirstLetterSpecification = dojo.declare( schkovich.CompositeSpecification, { constructor: function(firstLetter) { this.IsSatisfiedBy = function(entity) { return entity.firstName && (entity.firstName.indexOf(firstLetter) === 0); } } });
Once we defined conditions we will put them in a single specification:
var spec = new EpochSpecification("Romantic"). Or(new EpochSpecification("Post-Romantic")). And( new FirstLetterSpecification("G") .Or(new FirstLetterSpecification("C")) );
Finally we need a function that will filter data:
var filterComposers = function(specification) { var filtered = dojo.filter(composersData.items, function(entity){ return specification.IsSatisfiedBy(entity); }); composersData.items = filtered; return composersData; }
Here is the HTML page that puts all of the above together:
<!DOCTYPE html> <html> <head> <title>The Dojo Toolkit:: The Specification Pattern</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css" /> <style type="text/css"> @import "http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox/grid/resources/claroGrid.css"; html, body { width: 100%; margin: 0; font-family:helvetica,arial,sans-serif; font-size:90%; } .dojoxGrid table {margin: 0;} #gridPlaceholder {height:314px; width: 50%} </style> </head> <body class="claro"> <div id="gridPlaceholder"></div> <script type="text/javascript"> djConfig={ parseOnLoad: false, isDebug: false, baseUrl: "./", modulePaths: {schkovich: "./resources/schkovich" } }; </script> <script src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js"> </script> <script> dojo.require("dojo.data.ItemFileReadStore"); dojo.require("dojox.grid.DataGrid"); dojo.require("dijit.form.Button"); dojo.require("schkovich.CompositeSpecification") dojo.addOnLoad(function() { dojo.parser.parse(); var composersData = { identifier: "id", label: 'firstName', items: [ {"id":"1","firstName":"Johann Sebastian","lastName":"Bach","category":"Baroque"}, {"id":"2","firstName":"Arcangelo","lastName":"Corelli","category":"Baroque"}, {"id":"3","firstName":"George Frideric","lastName":"Handel","category":"Baroque"}, {"id":"4","firstName":"Henry","lastName":"Purcell","category":"Baroque"}, {"id":"5","firstName":"Jean-Philippe","lastName":"Rameau","category":"Baroque"}, {"id":"6","firstName":"Domenico","lastName":"Scarlatti","category":"Baroque"}, {"id":"7","firstName":"Antonio","lastName":"Vivaldi","category":"Baroque"}, {"id":"8","firstName":"Ludwig van","lastName":"Beethoven","category":"Classical"}, {"id":"9","firstName":"Johannes","lastName":"Brahms","category":"Classical"}, {"id":"10","firstName":"Francesco","lastName":"Cavalli","category":"Classical"}, {"id":"11","firstName":"Fryderyk Franciszek","lastName":"Chopin","category":"Classical"}, {"id":"12","firstName":"Antonin","lastName":"Dvorak","category":"Classical"}, {"id":"13","firstName":"Franz Joseph","lastName":"Haydn","category":"Classical"}, {"id":"14","firstName":"Gustav","lastName":"Mahler","category":"Classical"}, {"id":"15","firstName":"Wolfgang Amadeus","lastName":"Mozart","category":"Classical"}, {"id":"16","firstName":"Johann","lastName":"Pachelbel","category":"Classical"}, {"id":"17","firstName":"Gioachino","lastName":"Rossini","category":"Classical"}, {"id":"18","firstName":"Dmitry","lastName":"Shostakovich","category":"Classical"}, {"id":"19","firstName":"Richard","lastName":"Wagner","category":"Classical"}, {"id":"20","firstName":"Louis-Hector","lastName":"Berlioz","category":"Romantic"}, {"id":"21","firstName":"Georges","lastName":"Bizet","category":"Romantic"}, {"id":"22","firstName":"Cesar","lastName":"Cui","category":"Romantic"}, {"id":"23","firstName":"Claude","lastName":"Debussy","category":"Romantic"}, {"id":"24","firstName":"Edward","lastName":"Elgar","category":"Romantic"}, {"id":"25","firstName":"Gabriel","lastName":"Faure","category":"Romantic"}, {"id":"26","firstName":"Cesar","lastName":"Franck","category":"Romantic"}, {"id":"27","firstName":"Edvard","lastName":"Grieg","category":"Romantic"}, {"id":"28","firstName":"Nikolay","lastName":"Rimsky-Korsakov","category":"Romantic"}, {"id":"29","firstName":"Franz Joseph","lastName":"Liszt","category":"Romantic"}, {"id":"30","firstName":"Felix","lastName":"Mendelssohn","category":"Romantic"}, {"id":"31","firstName":"Giacomo","lastName":"Puccini","category":"Romantic"}, {"id":"32","firstName":"Sergei","lastName":"Rachmaninoff","category":"Romantic"}, {"id":"33","firstName":"Camille","lastName":"Saint-Saens","category":"Romantic"}, {"id":"34","firstName":"Franz","lastName":"Schubert","category":"Romantic"}, {"id":"35","firstName":"Robert","lastName":"Schumann","category":"Romantic"}, {"id":"36","firstName":"Jean","lastName":"Sibelius","category":"Romantic"}, {"id":"37","firstName":"Bedrich","lastName":"Smetana","category":"Romantic"}, {"id":"38","firstName":"Richard","lastName":"Strauss","category":"Romantic"}, {"id":"39","firstName":"Pyotr Il'yich","lastName":"Tchaikovsky","category":"Romantic"}, {"id":"40","firstName":"Guiseppe","lastName":"Verdi","category":"Romantic"}, {"id":"41","firstName":"Bela","lastName":"Bartok","category":"Post-Romantic"}, {"id":"42","firstName":"Leonard","lastName":"Bernstein","category":"Post-Romantic"}, {"id":"43","firstName":"Benjamin","lastName":"Britten","category":"Post-Romantic"}, {"id":"44","firstName":"John","lastName":"Cage","category":"Post-Romantic"}, {"id":"45","firstName":"Aaron","lastName":"Copland","category":"Post-Romantic"}, {"id":"46","firstName":"George","lastName":"Gershwin","category":"Post-Romantic"}, {"id":"47","firstName":"Sergey","lastName":"Prokofiev","category":"Post-Romantic"}, {"id":"48","firstName":"Maurice","lastName":"Ravel","category":"Post-Romantic"}, {"id":"49","firstName":"Igor","lastName":"Stravinsky","category":"Post-Romantic"}, {"id":"50","firstName":"Carl","lastName":"Orff","category":"Post-Romantic"} ] }; var EpochSpecification = dojo.declare( schkovich.CompositeSpecification, { constructor: function(epoch) { this.IsSatisfiedBy = function(entity) { return entity.category && (entity.category === epoch); } } }); var FirstLetterSpecification = dojo.declare( schkovich.CompositeSpecification, { constructor: function(firstLetter) { this.IsSatisfiedBy = function(entity) { return entity.firstName && (entity.firstName.indexOf(firstLetter) === 0); } } }); var filterComposers = function(specification) { var filtered = dojo.filter(composersData.items, function(entity){ return specification.IsSatisfiedBy(entity); }); composersData.items = filtered; return composersData; } var layout = [ { name: "First Name", field: "firstName", width: "40%" }, { name: "Last Name", field: "lastName", width: "40%" }, { name: "Epoque", field: "category", width: "20%" } ]; var spec = new EpochSpecification("Romantic"). Or(new EpochSpecification("Post-Romantic")). And( new FirstLetterSpecification("G") .Or(new FirstLetterSpecification("C")) ); var store = new dojo.data.ItemFileReadStore({ data: filterComposers(spec) }); var grid = new dojox.grid.DataGrid({ store: store, structure: layout, clientSort: true, rowSelector: '20px', query: {firstName: "*"} }, dojo.doc.createElement("div")); dojo.place(grid.domNode, dojo.byId("gridPlaceholder"), "first"); grid.startup(); }); </script> </body> </html>
In reality I been using the Specification Pattern to handle highly complex forms having predefined sequence or entering data and numerous validation conditions. In short, each change of input data will trigger a loop over the form elements making sure that each element meets specification.
I am also considering creating data store based on the Specification Pattern: Instead passing complex queries a Specification will be passed.