Thursday, January 20, 2011

Specification Pattern in Dojo

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.

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.

No comments:

Post a Comment