Rails on Rules: Example
June 25, 2007
I was just speaking with my friend Sami Samhuri about use cases for the rule engine. I imagined this scenario:
You've got a page that lists objects that are authored by many people. You have [a set of] reusable components/helpers/partials for displaying the objects in the list.
You want edit/delete links to appear for objects that were authored by the current user. You could imagine something like this in the template that uses the reusable component or in the reusable component itself:
if object.owner == session.current_user { display_edit_and_delete_links }
That's easy to code and maintain, and it can be refactored into a controller without much hassle.
But you might have dozens of pages that list completely unrelated types of objects. And then you get a requirement to add support for editorial workflow. Now you need to allow other people to edit/delete the object, not just the owner. The business rules (you see how this makes sense?) is not longer as simple as "if object.owner == session.current_user", but they do apply application-wide.
Change the "if" check to:
if context.shouldDisplayEditAndDeleteLinks { display_edit_and_delete_links }
Now the business logic is decoupled from the display logic, and the decision making can make augmented without changing application code. Just provide rules:
Note, the scenario below is simple, clearly a more robust "role" design would be desirable - but part of the point is "role" is an application concept, not a rule engine concept. Also note the (numbers) next to the rules: these are the priority of the rule, which make it easy to say one rule is more important than another.
Base rules that specify the role in the current context:
*true* => role = 'public' (10)
(session.current_user.is_editor = true) => role = 'editor' (50)
(session.current_user.is_manager = true) => role = 'manager' (60)
Base/original rules for displaying edit and delete links:
*true* => shouldDisplayEditAndDisplayLinks = false (10)
(object.owner = session.current_user) => shouldDisplayEditAndDisplayLinks = true (50)
New rules to support editors editing objects:
(role = 'editor') => shouldDisplayEditAndDisplayLinks = true (100)
New rules for important exceptions to the above:
(entity.name = 'SecurityNotice') => shouldDisplayEditAndDisplayLinks = false (1000)
((entity.name = 'SecurityNotice') and (role = 'manager')) => shouldDisplayEditAndDisplayLinks = true (1010)
If we weren't using rules, we would have had to write some pretty complicated code to cover all of these (and future) cases, and duplicate that code in many places or augment the application design (in many places) to inherit/mixin the code, or augment the application design (in many places) to have an entry point that could call a centralized routine that implemented all that logic.
When you think of all the places in your application you have complicated business rules that require application logic to implement, and how much the support of these business rules governs your application design to make it easy to implement/maintain, it becomes easy to see how this approach simplifies application design and improves productivity.
June 6, 2007
FYI, rules look something like this:
*true* => itemHelperName = "DefaultItemHelper"
(task = 'inspect') => itemHelperName = "InspectItemHelper"
(task = 'list') => itemHelperName = "ListItemHelper"
((task = 'list') and (entity.name = 'Post')) => itemHelperName = "MyCustomListPostHelper"
Note that this example is VERY contrived for the purposes of a really simple example. Presumably the first three rules would be supplied by a system that included generic scaffolding templates, and the last rule would be supplied by the application developer to provide more specific blog listing rendering functionality.
Also note that I'm reusing some of Direct To Web's terminology above, specifically "task" and "entity".
See Also: Rails on Rules: Background
Meaning: If a page listing objects from a collection wants to know which helper to use to render the item, it should use the ListItemHelper, unless it is listing blog posts, in which case it should use a MyCustomListPostHelper renderer. If a template that just shows (inspects) a single object needed a helper, it would use an InspectItemHelper. For other "tasks", the template should use a DefaultItemHelper.
The template would look up the value of "itemHelperName" during rendering via a "Context" object.
Rules have a left hand side (LHS) which is the boolean expression that determines if a rule should fire, and a right hand side (RHS) which specifies which key the LHS applies to, and what value should be returned when the rule is fired.
I've left out a few details about how the rules work in the examples above. For example, the value isn't limited to static values, you can specify an assignment class along with a rule which can supply a value when the rule is fired. Also, rules are given a priority so that the system can determine which rule should fire in the case that more than one rule could apply. Of course, base rules are given a lower priority than developer-supplied rules.