Last week I posted about creating a blog using KRL as an example of an application that used multiple rulesets and has a more complicated event hierarchy than typical KRL applications. I followed that up with a post about making the back button work in the resulting blog as an example of using external JavaScript libraries and emitting raw JavaScript from rules. We even added a new event type (web:hash_change
) using JavaScript. You can see the resulting blog here.
Another idea I've wanted to explore is how event-driven applications can be extended in a loosely coupled way. The argument is that because event-driven applications don't rely (as much) on request-response (remote RPC) interactions, the should be more loosely coupled. Functionality can be layered onto event-drive applications in ways that's difficult to imagine in more tightly coupled architectural styles.
If you're familiar with hooks in programming languages, then you're part-way to understanding this. I'm most familiar with them from Emacs Lisp. Hooks provide places for intercepting control flow in the program. For example, when you load a major mode in Emacs Lisp, there is a hook that will call a user-provided function, allowing the mode to be configured and customized. Hooks usually cause functions to execute. Events, on the other hand, provide a level of indirection so that what runs can be dynamically determined.
Tweeting a Post
To put this into action in KRL, I expanded the blog application by layering on functionality that gave the author the option of automatically tweeting the blog post. If you recall from the previous design, when the form is submitted, a rule, handle_submit
raises the event new_article_available
. We can post the content of the post to Twitter by listening for that event and doing what's necessary to format and send the information to Twitter:
rule send_tweet { select when explicit new_article_available pre { author = event:param("postauthor"); title = event:param("posttitle"); tweet = << New blog post from #{author}: #{title} /kblog >>; } if (twitter:authorized()) then { twitter:update(tweet); } }
The rule send_tweet
listens for the new_article_available
event and uses the built-in KRL Twitter module to check the keys (stored in the key
pragma of the meta
section of the ruleset) and update the associated Twitter stream. This was pretty easy to put into place because the event was already being raised.
Making Tweeting Optional
The next step--making tweeting the post optional--however, revealed the mistakes of my earlier design that got in the way of modifying it by simple adding new rules rather than modifying the original rules.
Making Twitter update optional requires us to give the author the option of tweeting or not. Adding a checkbox to the form give the author this option and is relatively easy if an event reveals when the form is there. In my original design, the place_form
rule was a terminal in the event hierarchy, meaning that it raised no events. Without an event, there's nothing that the rule adding the checkbox can watch for. So, I modified the place_form
rule to raise the explicit event post_form_ready
. Now the place_checkbox
rule can listen for that event and add a checkbox to the form:
rule place_checkbox { select when explicit post_form_ready pre { form_id = event:param("form_id"); checkbox_html = << <p class="checkbox"> <label for="posttitle">Post to Twitter?</label> <input id="tweet" value="1" type="checkbox" checked="checked"> </p> >>; } after(".text-area", checkbox_html); }
This adds a checkbox to the form without modifying the original ruleset (besides the fix to raise the event). Other changes to the form for other features could also be added. The form now becomes a canvas that rules can paint as they modify the functionality of the underlying application.
(click to enlarge)
There's just one more tiny problem. The handle_submit
rule raises the new_article_available
event, but it places the form values in individual parameters. In other words, it's tightly coupled to the original structure of the form and thus won't pass on the value of the checkbox so that we can test it in the rule conditional. The answer is to be less specific in the form parameters we pass with the event. Here's the new handle_submit
:
rule handle_submit { select when submit "#blogform" always { raise explicit event new_article_available for ["a16x89", "a16x91"] with post = event:params(); } }
Notice that where before, the explicit event had a separate parameter for each form element, this version just passes all of the parameters onto any listening rules. Now send_tweet
can pick out what it needs:
rule send_tweet { select when explicit new_article_available pre { post = event:param("post"); author = post.pick("$..postauthor"); title = post.pick("$..posttitle"); tweet = << New blog post from #{author}: #{title} /kblog >>; } if (post.pick("$..tweet",true).length() && twitter:authorized()) then { twitter:update(tweet); } }
This rule is largely the same except that it has to pick the author and title out of the post parameters. The rule condition has been changes to check the value of the tweet
parameter in the form. A bit of explanation: the second, true
argument to pick
causes it to always return an array. We check it's length since a checkbox doesn't return anything if it's not checked and thus we get an empty array out of pick
. The length of an empty array is zero, which is interpreted as false
in KRL. We don't actually care about it's value if not zero.
Lessons Learned
The new ruleset consists of two rules: place_checkbox
for putting the checkbox in the form and send_tweet
for composing and sending the tweet if the checkbox is checked. The new rules overlay the existing application nicely. If they're not present then the functionality isn't either. The application keeps on working just as it did before. Adding the functionality requires no configuration of the original application or wiring the new functionality in place. Everything fits nicely together and comes apart just as easily.
Of course, that assumes that original application is written in a way that supports loose coupling. We ran into two problems:
- Terminal rules reduce chances for loose coupling Because the
place_form
rule was terminal--didn't raise any events to indicate what it had done--there was no way for another ruleset to extend it's functionality. The lesson here is that terminal rules should be avoided if your goal is to create extensible, loosely coupled applications that don't require code modifications. That said, as long as you have access to the code, adding explicit events to rules when you need them isn't difficult and is unlikely to break anything, so it's a low-cost, low-risk code mod. - Specificity in parameters leads to tight coupling When events have parameters, we should send them on as a map (hash). Picking out individual parameters and sending them on by name means that only those parameters will ever be available to other rules. That doesn't mean you can't filter parameter maps to remove data you don't want available downstream. But the result ought to be everything but the data you filter, not just the data you choose to pass on. Pass parameters as a structure rather than by name for support the greatest flexibility.
These problems are easy to find and a simple test application showed where they existed. I believe that writing applications as collections of rulesets in such a way that they support extending functionality through accretion in a loosely coupled manner is practical, but it does require some planning and design effort.
Event Hierarchy
The modification to the event hierarchy is fairly modest since only two new rules were added. This reflects the loosely coupled nature of the application.
(click to enlarge)
There is a new ruleset for the tweet rules. The place_form
rule now raises the post_form_ready
event for which the place_checkbox
rule is listening. The send_tweet
rule listens for the new_article_available
event. As they should, the new rules are accretive to the overall event hierarchy, not requiring changes to it, but merely adding to it.