Reconsidering Controllers - Part 2
To review quickly, for some simple "query" controller methods, you could describe them in terms of an action with a name (for calling), the object (to query), the method name (to call on the object service class) and 0..n parameters containing static properties (the properties to display in an event list) or dynamic properties (such as the ID of the user to display the events for depending on the SiterUser.ID runtime property). Here an an example:
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
Queries vs. Commands
This is fine as far as it goes, but you couldn't build much of a web application with this. Most web applications contain a combination of queries and commands. A query is an action designed to display some facet of the state of an application without affecting that state (the line is a little blurry as of course an app might capture user history which means even queries will affect state, but the intent of the query is just to view state). Commands are typically designed to change the state of an application (think add, edit, delete, import, etc.). Of course, when you access a web application using a browser you usually want to either run a query or a command followed by a query otherwise you won't see anything on your screen.
Let's look at some more sample actions including some that are commands:
| Feature | Action | Object | Description |
| Cart | Add | Cart | Add 1..n cart items to current site users cart. Then Cart.View |
| Cart | Update | Cart | Update item quantities from current site users cart based on form. Then Cart.View |
| Cart | Delete | Cart | Delete the selected item(s) from current site users cart. Then Cart.View |
| Authentication | Display Login | n/a (Site User) | Display a login form |
| Authentication | Process Login | Site User | Validate siteuser. If valid, continue with primary action. If not, redisplay login form and message |
| Authentication | Process Register | Site User | Save siteuser. If valid, go onto another specified action. If invalid, redisplay register screen with error message. |
| Authentication | Display Forgotten Password | n/a (Site User) | Display |
| Product Admin | Product List | Product | Get list of products (with any necessary filter, order and paging) and display in product list screen. |
| Product Admin | Display Add Form | Product | Get a new() product bean with any default properties and display it in a form. |
| ProductAdmin | Process Add Form | Product | Save/validate product. If valid, go to another action (maybe display product list). If invalid, display product form screen with values entered (the bean we just tried to save) and error message. |
| Product Admin | Display Edit Form | Product | Get the requested product bean (based on ID, SKU, etc.) and display it in a form. |
| Product Admin | Process Edit Form | Product | Save/validate product. If valid, go to another action (maybe display product list). If invalid, display product form screen with values entered (the bean we just tried to save) and error message. |
| Product Admin | Request Product Delete | Product | Get the product(s) you want to delete and display their titles in an "are you sure" screen. |
| Product Admin | Execute Product Delete | Product | Delete the product(s). If able to delete, display another action (e.g. product list) with a success message. If unable to (maybe because of dependency constraints), display another action (might also be product list) with a failure message. |
| Product Admin | Review Product | Product | Get product and display it on an "approve" screen. |
| Product Admin | Approve Product | Product | Try to approve product. If succeed, display another action (e.g. product list) with a success message. If unable to (security or other issues), display another action (might also be product list) with a failure message. |
Many Commands Require Conditionals
With these samples, we're now starting to see commands - actions designed to modify system state - often by adding, editing or deleting one or more object instances. What is different? The first thing that doesn't fit into our simple DSL is the conditionality of the action. In general terms our statements are now looking more like:
Set Optional Parameters
Do %Command%
If %Condition%
either %Action% or %View%
Else
either %Action% or %View%
Where Command is calling a method on an objects service class, Condition is some sort of logic - probably based on the value returned by the Command, Action calls another action and View just sets the "screen-name" and returns the results of the command plus the screen name to the framework that called the controller.
Let's look at one way of expressing this for (say) adding products. Firstly we'd need a simple query action for displaying the product add form - let's call it ProductDisplayAddForm:
action: name="ProductDisplayAddForm" object="Product" method="new"
param: name="PropertyNameList" value="Title,Price,Description,MainImage"
view: name="ProductForm"
/action
A quick note here. The problem with putting things like the PropertyNameList here in the controller is that it isn't very DRY as now if we want to have a flex interface for adding new products we're going to have to replicate the PropertyNameList, so while parameterizing the property name list here should be an option, in practice I might create a virtual method within the service class (let's call it newProduct()) which would KNOW what properties were required when adding a new product in which case our controller virtual method (action) would be replaced with:
action: name="ProductDisplayAddForm" object="Product" method="newProduct"
view: name="ProductForm"
/action
Note we've just got rid of the PropertyNameList parameter and called a newProduct() method which is presumably a parameterized call to new() which has the PropertyNameList pre-set so that knowledge is contained within the model - not the controller.
So, we have a "display add form" action which calls ProductService.call(new) (or ProductService.call(newProduct)), returning a new Product bean with any default values set for the appropriate properties and then passes that to the ProductForm screen which displays the fields for that form based on the PropertyNameList. Then we need some kind of action for processing the "add" form. Let's call it ProductProcessAdd:
action: type="save" name="ProductProcessAdd" object="Product" method="add"
param: name="PropertyNameList" value="Title,Price,Description,MainImage"
/action
The "type" attribute tells it that this is some kind of save method so it is going to have to call ProductService.call(new) to get a bean and then Product.loadStruct(Input.asStruct(), PropertyNameList) which basically means it needs to take the input bean containing form and URL elements as a struct and the PropertyNameList of fields to save and pass them both into the loadStruct method of the transient Product bean (which is actually a decorated Product bean where the decorator handles controller and view concerns). That is probably a little more about how I do things than you want to know, but the important thing is this ProductProcessAdd method knows how to save the product. All the "type" attribute does is tell the feature controller what "real" method to call using the parameters in the DSL to actually make this work at runtime. A BaseFeature includes a bunch of core methods like list() and add() and you can create custom Feature classes with any methods that really do require code so you can create your own class of parameterizable methods for your use cases.
OK, so back to our ProductProcessAdd action. Depending on the results, it needs to do one of two things, so we need to describe that conditionality:
action: type="save" name="ProductProcessAdd" object="Product" method="add"
param: name="PropertyNameList" value="Title,Price,Description,MainImage"
if: expression="%Product.Valid%" action="ProductList"
if: expression="NOT %Product.Valid%" view="ProductForm"
/action
Are you SURE You Want to do This?!
Whenever you start adding logical or looping constructs to a DSL it is time to step back and look at whether you're going down the wrong road. One of the biggest risks with any domain specific language is that it will eventually evolve into a (badly designed) general purpose programming language, so whenever you take one of the big "Towards Turing (complete)" steps you really want to ask if it is a good idea.
In this case it is clear that the domain language requires some kind of conditionality. Talk to a client and they'll say something like "if the user is authenticated, go here otherwise take them back to the login form" or "if the product is valid, display a list of products otherwise re-display the product form with an error message", so we do need conditionality. The other question then is whether we really want to be building a DSL. For my use case the answer is "yes" as for the levels of reusability and efficiency I really need to capture this in terms of a DSL so I can tie actions into a feature model and assign them easily to a project. I also want these kind of fundamental business requirements to be stored in an abstract format so they could be generated into any appropriate programming language if required rather than having them encoded in a language specific syntax that'd be much harder to transliterate to other languages automatically.
Expressing Conditionality
So, we have a DSL, it needs conditionality. What is the best way to express that? Obviously there are lots of ways of doing this that we are familiar with. The first options that come to mind are if, if/else and switch/case. Let's look at a sample of each:
If:
if: expression="%Product.Valid%" action="ProductList"
if: expression="NOT %Product.Valid%" view="ProductForm"
IfElse:
If: expression="%Product.Valid%"
action="ProductList"
Else:
view="ProductForm"
Switch/case:
switch expression="%Product.Valid%"
value="true" action="ProductList"
value="false" view="ProductForm"
which could also be expressed:
switch expression="%Product.Valid%"
value="true" action="ProductList"
defaultcase view="ProductForm"
The first option is simple, but annoying if you ever need else or switch like conditions as you need to keep re-stating the expression which isn't very DRY. The second is good if you only ever have two possibilities or ever need sub if's (if this is true, then if something else is true do a else b), the third is ideal if you have n-options where n >2. I think I'm going to go for the third option with support for a default case as it is nice and easy to parse and read and a little more flexible than #2. If I find I need sub-switches I'll refactor the XSD to allow for not just a simple action or view but additional switch statements within the case of a given switch, but I don't thing I need that right now.
So, now to express the action for processing a product add form, we have:
action: type="save" name="ProductProcessAdd" object="Product" method="add"
param: name="PropertyNameList" value="Title,Price,Description,MainImage"
switch expression="%Product.Valid%"
value="true" action="ProductList"
defaultcase view="ProductForm"
/switch
I still don't think this is a complete solution, but it is starting to cover a wider range of use cases. Keep tuned for part 3 where I look at some more possible enhancements to this to see if I can get something that'll allow me to solve most of my use cases without coding.
Thoughts?



This allows for things a controller should do, but doesn't require you to implement ifs, cases, and loops.
Of course, knowing what I know about what you are trying to accomplish, it could get quite tedious to implement the DSL in multiple languages. Sure - the language is quite small, but even that can get annoying.
On the other hand, you'll have to implement it somehow in each language - so it becomes just a question of when and where - and then which approach has more benefit than the other.
Perhaps you could expand on why, in this case, you chose the path of implementing the full scale external DSL and the code generates in each language you want to support, rather than implementing it internally in several languages. (Did having a common syntax play a large role?)
In short, because I want to be able to write my controllers once and then deploy them using any programming language. Sure, I need to write the framework once in each language, but if I want to port 500 sites from one language to another in an afternoon, I'd rather write code to interpret XML in each language than try to write a Java to Ruby transliterator that supports all the possible language constructs you might have used around your internal DSLs.
Prior to that, I thought each particular app could be deployed one way - I had forgot that each could be deployed more than once to more than one language.
Thanks for the reminder.