In a book entitled Finite and Infinite Games in 1986, James P. Carse wrote "Finite players play within the rules, infinite players play with the rules." We play finite games every day, from checkers and chess to Yatzee and Monopoly. Finite games have a familiar pattern: a beginning, a middle, and an end; a winner and a loser.
A finite game is easy to play because it has a limited set of fixed parameters. Carse also wrote "Finite players play to win, infinite players play to keep playing." Custom software development is not a finite game, and the clients who purchase it are not finite players. The objective of a workflow application is not to win an office betting pool, or even to design and complete a superior workflow application. The objective of a workflow application is to continue to distribute an ever-changing flow of work. Yet as custom software developers we tend to play the infinite game with finite rules.
We shouldn't be taking a request from a client to change the way our application determines how work is distributed (15-day expiration instead of 30-day expiration, apply special notices to tasks above or below a particular dollar value which is later changed, etc). Instead we should be providing our clients with the tools to change these business rules themselves, so that instead of constantly trying to keep up with the stream of client change requests that result from hard-coded business logic, we can move on to the next feature or application, and play the infinite game on their terms, playing with the rules rather than playing within them.
A Brief Example
A typical business rule should be familiar to all of us and is usually described to us by a client with words like this: "commissions for this project are due to the sales person 15 days after receipt of at least 10 percent of the deposit." This rule has one objective (indicating current payments due to the sales staff) and several parameters, including the required deposit amount, an arbitrary figure (10%), the date on which the figure is satisfied, an arbitrary duration (15 days), and by implication, the current date. These are typical logical equations and we know intuitively how to write software to perform these comparisons. The code usually looks something like this:
<cfif sale.getDepositPaidByDate(dateAdd("d",-15,now()))
gte (0.1 * sale.getFullDepositAmount())>
As long as the application you're writing will be sold only to one client, and that client never changes their rules, this line of code will survive the lifespan of our application. The problem with this is that, while it may satisfy the client it doesn't help us much. It doesn't allow us to sell the same application to another client without reinventing this portion of the code and so at the end of the day it amounts to being paid an hourly wage for your work instead of building equity in your software.
The Challenge
There are two barriers to transforming this finite equation into a flexible rule that many of your clients can modify without your aid or intervention:
A single "RuleManager" CFC manages all interactions with the rule data in this application. In Object Oriented (OO) lingo this is known as a Façade pattern, in which a larger library of code is simplified by a single unified interface. Although the RuleManager will require a minimal set of assumptions, certain assumptions must be made in its design to ensure portability:
A Sample RuleManager Component
Due to publishing constraints I've simplified the sample RuleManager code quite considerably for this article. For example, the sample RuleManager doesn't provide any features for dynamically loading or unloading different criteria types or groups of criteria types - instead it merely examines a directory for CFCs and assumes each is a criteria type CFC. I've also eliminated debugging features, a considerable amount of i18n functionality for multilingual applications (which is no small challenge to implement with a RuleManager), simplified the provided criteria CFCs, and omitted nearly a dozen common criteria types. One feature I'm particularly sorry to omit is the ability for individual criteria CFCs to introspect the provided rule context to determine their own applicability to the context. Although I feel this is an important feature for the sake of encapsulation and extensibility, it would simply require too much space to include in this article.
Having trimmed the code for the RuleManager sample to a bare minimum, here are the absolutely necessary features of the façade (RuleManager.cfc) for this application to function:
string getXMLNode(nodeData): Once the criteria form is submitted it is necessary to return to the criteria-type CFC to determine the syntax for the criteria node before updating the ruleset XML document. Called within RuleManager.setCriteriaNode.
Making Sense of It All
To put this all in perspective, what I'm describing is for many of us a radical shift from the world of billing for custom-software to the world of licensing self-serve enterprise applications. An email client is a good example of the need: if you designed a webmail client you might want to provide the ability to automatically filter incoming messages into different folders when mail is downloaded, however you wouldn't want to be responsible for changing the filters for each of your clients. Instead you would create a flexible filtering engine to allow individual clients to select the criteria on which messages are filtered. This is the sole purpose of the RuleManager application: to provide a flexible interface for users to assign the criteria on which other interactions occur within your application. With this in mind I'll address the application flow.
You operate a hosting company and provide email with a built-in webmail interface. A user signs up for hosting and logs in to the webmail application. Within the application they create several folders for their incoming email, then want to assign filters to automatically move incoming email into these folders when messages are downloaded. After selecting the "filters" link they are presented with an empty list page, and a link to add a new filter (index.cfm).
Once the user selects the "add new filter" link they are brought to the edit-filter page (ruleedit.cfm) where they are presented with a simple form allowing them to provide a name and a description for their new filter. Once they've saved this basic rule information they are returned to the same page with an additional form below, containing a list of different types of criteria that they can add to their new rule. This list of criteria is provided by the RuleManager.cfc, which was probably created in the session scope when they logged in (since email rules only apply to the current user).
After selecting a criteria type (Text) and selecting the "Add Criteria" button the user is again returned to the same edit-filter page. At this point a third form is presented to the user below the previous form for general filter information (name, description) and the "add criteria" form. Although the third form is ultimately created by a criteria.text.cfc object the RuleManager.cfc façade simplifies this process by instantiating the desired criteria-type CFC and calling its criteria.getForm() method within RuleManager.getCriteriaForm(). This encapsulates a significant amount of the functionality of determining the location of the criteria-type CFC and caching it in memory so that individual pages can treat the RuleManager and its criteria as a "black box."
The presented form is unique to Text criteria types - you can see this by selecting the Date criteria type in the "add criteria" form. The Text criteria form contains three elements - text to search (Sender, Subject, Message, Recipient), a user-defined expression, and a selection to determine how the expression is matched against the text (comparison). Although the expression is entered by the user and the types of comparison are defined by the criteria-type CFC, the list of available text properties that can be matched with a Text criteria are provided by the "email" context (EmailRuleContext.cfc).
When the user selects the "apply criteria" button the form is submitted to the criteria-update template (criteriaupdate.cfm). Like the criteria form, the RuleManager façade also simplifies this update template by providing a singular interface to inject the new (or modified) criteria into the specified rule. This is handled by the RuleManager.setCriteriaNode() method. This method internally determines which criteria-type CFC is needed (stored in memory before displaying the form) and uses the appropriate CFC object to generate the XML for the criteria node, which is then injected into ruleset XML document the RuleManager uses as the format for its rules. The RuleManager object doesn't know where this XML document will be stored (file or database - the sample documents use a file, although in a real application email filters would likely be stored in a database), so the criteria-update template then stores the updated XML document and returns the user to the edit-rule page.
After applying the first criteria for the new rule, the user is presented with the general-information and add-criteria forms and a list of the criteria attached to the current rule (with links to edit or delete each criteria). Again because there is no way of knowing how many types of rule criteria may exist or apply to a given rule, it is necessary for the RuleManager façade to rely on the individual criteria-type CFCs to generate a text description of the applied criteria for the user to read. These text-descriptions of the mechanics of each rule-criteria could be omitted on pain of many sleepless nights with technical support calls, however as most of us enjoy our sleep the RuleManager.cfc again determines the appropriate criteria-type CFCs for each applied criteria and uses their criteria.describe() methods to generate the query returned by the RuleManager.getRuleCriteria() method.
The user can now continue on to apply as many additional criteria as they like to their rules, such as the "or" criteria, which allows them to apply mutually exclusive sets of conditions in which the filter is applied. At this point however the entire workflow of the user-interface is satisfied. All that remains is the application or integration of the rules into the flow of the application.
In the sample case a webmail application would contain a template (or CFC method or custom tag) for downloading email from the server. In order to filter the incoming email messages into the appropriate folders, the download process must loop over the downloaded messages and for each message must loop over and test each of the user's email filter rules. Again the RuleManager façade simplifies the process by providing a single method, RuleManager.ruleApplies(ruleid [, context]), which loops over the criteria for an individual rule and tests the applicability of each criterion to provide a boolean value which may be used by the application to determine if the message is filtered.
In order for the ruleApplies method to function properly the context CFC must provide the actual property values to the criteria-type CFCs for each downloaded email. Thus when the criteria-type CFC calls the method context.getFromHeader() the context CFC must return the "Sender" string value "s. isaac dealey <info@turnkey.to>" for any message sent by myself. This can be accomplished either by instantiating a CFC for each message and passing that CFC as an optional second argument to the RuleManager.ruleApplies() method, or a single context CFC may use an iterator property to indicate the current row of a query from which email information is retrieved. Although for this application I would recommend the latter approach for the sake of efficiency, individual context CFCs may be more appropriate for other applications.
Unfortunately the scope of this article doesn't allow me the luxury of providing a complete sample application. The sample code (which is available by viewing this article online) and the above paragraphs lack an explanation of how the folder for each filter is identified. There are several methods that could be applied, including an additional attribute in the XML node for each rule or additional structure in your database. You will need to address these details case-by-case in your applications.
Conclusion
Although I lament that this article could not examine many of the more useful nuances of the RuleManager façade it should arm you with enough information to start providing powerful self-service customization to your clients today. The previous paragraphs and code samples provide information about basic concepts, storage, formatting, user-interface and application in business-logic, as well as thorough explanations of all the necessary CFCs and methods and a step-by-step explanation of the flow of user-interface and application logic. For questions or to see a more thorough example, you may contact me directly or view the onTap framework documentation at www.fusiontap.com.