December 16, 2004

OI2 actions are just plain old objects

“Think your objects are usable? Try instantiating them in a test framework.” I don’t remember where I read this statement but it’s something I strongly agree with. It’s not only so you can make your objects testable, it’s that in a testing framework you don’t typically have your framework or container built up my the myriad typical steps: like in Java – first Tomcat which creates a JNDI service and places in it your JDBC connection, and then Spring (or whatever) which reads in X other configuration files, and then…. A testing framework generally assumes none of that, drawing back all the layers until you’re exercising the actual object rather than any of its dependencies. If you’re able to easily instantiate your objects in a testing framework you’ve probably reached a good level of simplicity.

Most of OpenInteract2 adheres to this fairly well. The only caveat is that for many things to work properly you do need to instantiate the Context, but you don't have to do so in a web server or even a persistent environment. Of the other core pieces of the framework the one developers encounter most often is the Action. You might be surprised that these aren't tied to the web either -- they're just normal objects. Their primary purpose is to generate content, and there's nothing that says you have to be instantiated in a web server to do so.

One of their other purposes is to act as application integration points. Because Actions are observable we can use them to fire events that other classes or objects can catch. As an example: when I post this news article to my OI2 website, the 'News' action will fire a 'post add' event to all its observers. One of those observers takes the 'news' object given as part of the event, pulls content from it and posts that content to my use.perl journal. The 'News' action doesn't know it's being observed, the journal posting observer doesn't know who threw the observation -- nice and decoupled code.

But what happens when one of the observers doesn't fire properly? (For instance, when you learn that your observer assumes that it's running in a web environment...) The observation is gone, right? Not so fast -- remember, the observation is just a method call with particular parameters, and our 'News' action is just an object. So why not just create the object and make the method call with the same parameters?

As a corrollary to "Try instantiating them in a test framework" I add this: "Try instantiating them in a standalone script." Here's one such script using OI2 that takes the ID of the news object for which it should fire the 'post add' observation again:

#!/usr/bin/perl
 
use strict;
use Log::Log4perl qw( :easy );
 
Log::Log4perl->easy_init( $WARN );
 
my ( $news_id, $site_dir ) = @ARGV;
$site_dir ||= $ENV{OPENINTERACT2};
unless ( $news_id and $site_dir ) {
    die "Usage: $0 news-id [ site-dir ]\n",
        "   Can use OPENINTERACT2 env instead of 'site-dir'\n";
}
 
require OpenInteract2::Context;
my $CTX = OpenInteract2::Context->create({
    website_dir => $site_dir,
});
 
# Get the news object to fire observation for
my $news = OpenInteract2::News->fetch( $news_id );
unless ( $news ) {
    die "No news found with ID '$news_id'\n";
}
 
# Get our news action
my $news_action = $CTX->lookup_action( 'news' );
 
# Uncomment to use debug only
#$news_action->param( use_perl_debug => 'yes' );
 
# Fire the observation
$news_action->notify_observers( 'post add', $news );

Not too hard, and you know it works because my previous news posting got sent to use.perl properly! What's nice is that we're using the same methods and patterns that we'd use if we were running inside Apache/mod_perl or any other application container -- the application code is isolated from that, unless you want to be tied to your container. (Sidenote: I really like the easy_init() shortcut of Log4perl - you don't need a complicated configuration to use it in simple cases, but you still get all the benefits you need them.)

I'll talk more about how the observer works later.

Next: How to create inner class-like implementations in Rhino
Previous: Careful when using properties files to specify Ant taskdefs