By Peter Bell

Reconsidering Controllers - Part 3

In my first post, I looked at a basic DSL for simple "view" screens and in the second part I added support for conditionality ("if form submission valid do this, else do that"). In part 2.5 I made some comments about "types of elements" in DSLs. In the third part of this series, I start to look at some of the other issues that my controller DSL might have to consider by trying to "build" a simple commerce system using my DSL . . .

To reprise, by the end of the first posting we has a simple DSL allowing statements like:

action: name="CategoryView" object="Category" method="getByID"
param: name="IDList" value="%Input.CatID%"
param: name="PropertyNameList" value="Title,Description,MainImage,SubCategories,Products"
/action

or:

action: name="MyEventList" object="Event" method="getByFilter"
param: name="Filter" value="EventUserID = '%SiteUser.ID%'"
param: name="PropertyNameList" value="StartDate,EndDate,Title"
param: name="Order" value="StartDate DESC"
/action

Basically, an action has a name (for calling it), an object it was related to and a method to call on that object which could be a base method like getByFilter() or a custom method like getNewProducts() which could either be a virtual method (described using metadata as a parameterized call to a real method) or a physical method which was manually coded within a given objects service class.

The action can also have 0..n parameters which can include dynamic elements denoted by %scope.variable% - examples would be %SiteUser.ID%, %Input.ProductID%, %Page.Title% and %Request.Time%.

In the second posting I then added two new concepts. The first was an attribute to the action element called "type" to allow types of actions to take advantage of underlying controller code so it doesn't need to keep on being rewritten. Common types might be list, add, edit, delete, import, export, report, detail, etc., but a "type" is really just the method to call on the controller, so you'd be able to add your own types just by adding a method to a given feature class file.

The second element was support for a switch statement to capture different flows based on run time variables (such as whether a form submission was valid) and the idea that each case would support either another action or the name of a view to return. The sample given was:

action: type="save" name="ProductProcessAdd" object="Product" method="add"
param: name="PropertyNameList" value="Title,Price,Description,MainImage"
switch expression="%Product.Valid%"
case="true" action="ProductList"
defaultcase view="ProductForm"
/switch
/action

(I've renamed "value" to "case" at the start of line 5 as I find that more consistent.) I have a feeling a may need to refactor this to support each case including the defaultcase allowing for their own switch statements, but bearing in mind YAGNI I'm going to worry about that when it arises - it is something I could do if I needed to.

In part 2.5, I make each "type" of action it's own element to make the grammar easier to document and validate, so the statement now looks like:

save: name="ProductProcessAdd" object="Product" method="add" PropertyNameList="Title,Price,Description,MainImage"
switch expression="%Product.Valid%"
case="true" action="ProductList"
defaultcase view="ProductForm"
/switch
/save

The best way to test out a language is to write stuff in it as that very quickly brings up the edge cases that don't work, so here is a simple application with the controller described using this new DSL. Let's see what happens as we try to describe a real application . . .

Side note: what I LOVE about DSLs is that they are usually much more concise than a 3GL and it is a lot quicker to refactor a grammar and constraints than to refactor the code required to implement it, so I always try to write an entire application in a DSL before I write the interpreter or generator for it - it picks up so many issues so quickly that it really rocks.

Let's start with a simple commerce system. Lets include a catalog, cart, checkout and admin screens for categories and products. That obviously isn't a complete system, but it should be enough to tease out some interesting issues that I'm sure the simplistic DSL I've developed to date will need to be extended to support. (As before, I'm not interested in getting just the right features or functions for the e-commerce app, but rather I'm throwing together a plausible spec for the purposes of testing the flexibility of my controller DSL.)

So, lets have a CategoryView, CategoryProductView, ProductSearch and ProductView actions in the Catalog feature, and View, Add, Update and Remove actions in the Cart. I think that should be enough for this posting - I'll look at checkout and admin screens later today on my flight to New York.

Well, let's start with the basics by describing the Catalog with CategoryView, CategoryProductView, ProductSearch and ProductView actions.

view: name="CategoryView" object="Category" method="categoryView" propertyName="CategoryID" propertyValue="%input.CatID%" screen="CategoryView%Object.TemplateName%" /view

viewComposed: name="CategoryProductView" object="Category" method="categoryProductView" propertyName="CategoryID" propertyValue="%input.CatID%" composedRelationship="Products" composedPropertyName="ProductID" composedPropertyValue="%input.ProdID%" screen="ProductView%Object.Product.TemplateName%" /viewComposed

search: name="ProductSearch" object="Product" method="search" defaultRecordsPerPage="25" defaultOrderBy screen="ProductSearch" filter title="Keywords" PropertyName="*" field="textbox" size="10"
filter title="SKU" PropertyName="sku" field="textbox" size="10"
filter title="Category" PropertyName="CategoryList" field="DropDown" valueList="ProductCategoryList" multiple="0"
/search

view: name="ProductView" object="Product" method="getByProperty" propertyName="ProductID" propertyValue="%input.ProdID%" screen="ProductView%Object.TemplateName%" /view

There are a few things to note here. Firstly we have a separate element for view which returns an object with any composed objects as required based on a propertyName and runtime propertyValue - e.g. CategoryID 12 or CategoryName "Handbags" and for viewComposed which shows a single instance composed within its parent, and needs the composedRelationship, composedPropertyName and composedPropertyValue as well as the propertyName and propertyValue. One question you might ask is "why composedRelationship as opposed to composedObject?" Imagine a user has-many bosses (matrix management!) and has-many subordinates. If his bosses are UserID 12,13 and 14 and subordinates are UserID 21,22,23 and 24, you want to be able to distinguish the relationships even though they both relate to the same object. Trying to viewComposed composedRelationship="Boss" composedPropertyName="UserID" composedPropertyValue="%input.BossID%" would (quite correctly) only display Bosses if their ID was 12, 13, or 14. If we just has a composedObject="User" instead of the composedRelationship="Boss", you might potentially end up being able to display user 21 as this users boss. Also, different relationships may have different default field lists, admin rules and other properties.

Secondly, note that while the search element has a defaultRecordsPerPage and a defaultOrderBy attribute, it doesn't have a recordsPerPage, PageNumber or OrderBy attribute to describe the runtime variables used to determine each. Why? Because by convention all searches use the same variables for those, so we don't need to repeat ourselves for every search element (a simple, but useful convention). Also note that the search has 0..n filter elements allowing for fairly rich search tools to be described. (Should the SKU filter have a "size" attribute or is that a mixing of concerns between controller and view? Good question. For now I'm going to leave it in as otherwise I'd have to describe my filters both in the controller and the view which seems very un-DRY, but I'll see how it plays out with a couple of real application before I settle on an approach.)

Finally, note that I've added a screen property (duh - wouldn't be much use without that), and note a couple of things about how I handled screen names. ProductSearch has a static screen name in this case, but CategoryView has a screen name that is both a static value (CategoryView) and a dynamic part (%Object.TemplateName%). Basically what this means is to take the objetc returned by the method call and to set screen = CategoryView & #Object.get("TemplateName"). If the category doesn't have a template name, it'll just use CategoryView.cfm as the display screen for this content area. If it has one, it'll get concatenated onto the end based on the value of the TemplateName property of the Category object, so if that is set to "Specials", the screen for this content area will be CategoryViewSpecials.cfm. One more thing to not about this is the special case implementation for the ViewComposed element - CategoryProductView. The service method returns an Object IBO which is a Category and has a composed Product which in turn has a property called TemplateName. So screen="ProductView%Object.Product.TemplateName%" should be processed as Screen = "ProductView" & Object.get("Product").get("TemplateName"). That is pretty easy to parse and implement and means that even properties of composed objects can be used for dynamic properties or conditions which I think is quite nice. (Thoughts?!)

OK, so far so good. So, let's try to describe the View, Add, Update and Remove actions in the Cart.

The view action is pretty simple and similar to other views we've looked at earlier:

view: name="CartView" object="Cart" method="getSiteUserCart" propertyName="SiteUserID" propertyValue="%SiteUser.UserID%" screen="ViewCart" /view

Add to cart needs a new action type for "add composed" as we're trying to add items which are composed within a cart:

addComposed: name="AddtoCart" object="Cart" method="addComposed" propertyName="UserID" propertyValue="%SiteUser.UserID%" composedRelationship="CartItems" /addComposed

Update cart is really about editing composed objects (changing the quantities for objects, although setting quantity to 0 is something that has to be captured and handled correctly by deleting the instance).

editComposed: name="UpdateCart" object="Cart" method="editComposed" propertyName="UserID" propertyValue="%SiteUser.UserID%" composedRelationship="CartItems" /editComposed

I have a feeling that addComposed and editComposed are going to need some more attributes, but I think I might start to implement the interpreter and see what is required to make these screens run.

Remove from cart is simply a deleteComposed, which would probably look something like:

deleteComposed: name="RemovefromCart" object="Cart" method="deleteComposed" propertyName="UserID" propertyValue="%SiteUser.UserID%" composedRelationship="CartItems" deletePropertyName="CartItemID" deletePropertyValueList="#input.CartIDList%" /deleteComposed

Well, I think there is still a lot of work to do, but I think this is enough for this posting (and my flight is just about to board!). I think this is definitely the basis of a DSL that could be useful for describing and generating web applications quickly and efficiently when combined with the back end DSLs we already have.

Any thoughts?

Comments
BlogCFC was created by Raymond Camden. This blog is running version 5.005.