Brian's Waste of Time

Tue, 17 Aug 2004

Components, Design, and Functions

...or some rambling about IoC and design in webapps. Basically, I want to look at some idioms that have proven really useful for ubiquitous components in web applications over the last year or so.

First, nano got the container scopes exactly right, I think. The key here is three component containers -- one at the application scope, one at the session scope, one at the request scope, each has the previous as its parent. The application container is initialized and destroyed with servlet context startup/shutdown, the session with session creation/destruction/activation/passivation, and request container is rebuilt for each request.

So, lets look at an example, we'll steal a page from Matt Raible and simply look at a user registration/authentication use case. To accomplis this we need a few things -- a User class, a persistence system (Apache OJB), a use DAO (optional, but usually worthwhile), and a transaction system (awfully handy). Let's start at the lowest level and look at data access.

Because datastore and transactions are so closely tied together in most apps (not a bad thing, if you don't need distributed transactions, don't use em), we'll go ahead and build a TxRunner component called TxRunnerImpl which knows OJB. This component just knows about OJB, and can run Tx instances. Usage looks something like (credit a coworker of mine for a lot of the look of this particular runner, in a different form, for a different system):

return runner.execute(new Tx() {
    public Object execute(final PersistenceBroker broker) throws Exception {
        final Criteria criteria = new Criteria();
        criteria.addEqualTo("login", login);
        return broker.getObjectByQuery(QueryFactory.newQuery(User.class, criteria));
    }
});

This design doesn't hide that OJB is being used, if that needs to happen, you can pass some interface which knows how to execute queries, inserts, etc. I just expose the broker. The TxRunner wraps the Tx in a transactions, rolls back on exceptions, etc. It can also be very nicely matched by a cflow ;-)

Now, take a look back at the TxRunnerImpl and look at its constructors. It, really, isn't dependent on anything. When instantiated in unit tests the String constructor can be used, when in a container the ServletContext. I am open to arguments about allowing it to be dependent on the ServletContext, but it works well for auto-wiring. If this were Spring, I'd get rid of the ctx and just specify the string parameter in the config xml. Right now there is no configuration xml, and I don't want to add it for this.

Okay, lets now look at something to use this, our UserDAO implementation, UserDAOImpl. The impl depends on the TxRunner, declared in its lone constructor. It uses this in order actually find/insert/etc User instances. Nothing terribly fancy here. Do notice that it wraps exceptions in a service layer exception, again no biggie.

We have two final components, a Session data holder, and an Authenticator. The SimpleSession is dirt simple with no dependencies. It just holds session state (the logged in user). The SimpleAuthenticator is more interesting. It depends on the UserDAO and the Session components. It also, finally, provides a service method (authenticate(String,String): void) which uses those components (sets found user in session if valid, throws exception if invalid (you can argue this point as well, I like exceptions for auth failure, other people like returning error codes)). It is worth noting that the only reason there is an interface/impl seperation between these particular components is for unit testing (maniax is allowed to argue this one, but notice they are seperated ;-)

Okay, so we have these components, nothing too fancy anywhere here. What do we do with them? Let's see an app which can use them. The example will be a simple little nanoweb thing. We wire up the components as follows:

import org.skife.gear.Components

if(assemblyScope instanceof javax.servlet.ServletContext)
{
    pico =  new org.nanocontainer.reflection.DefaultSoftCompositionPicoContainer()
    pico.registerComponentImplementation(Components.USER_DAO,
                                         org.skife.gear.service.ojb.UserDAOImpl)
    pico.registerComponentImplementation(Components.TX_RUNNER,
                                         org.skife.gear.service.ojb.TxRunnerImpl)
    pico.registerComponentInstance(assemblyScope)
    return pico
}
else if(assemblyScope instanceof javax.servlet.http.HttpSession)
{
    pico = new org.nanocontainer.reflection.DefaultSoftCompositionPicoContainer(parent)
    pico.registerComponentImplementation(Components.SESSION,
                                         org.skife.gear.service.simple.SimpleSession)
    pico.registerComponentInstance(assemblyScope)
    return pico
}
else if(assemblyScope instanceof javax.servlet.ServletRequest)
{
    pico = new org.nanocontainer.reflection.DefaultSoftCompositionPicoContainer(parent)
    pico.registerComponentImplementation(Components.AUTHENTICATOR,
                                         org.skife.gear.service.simple.SimpleAuthenticator)
    pico.registerComponentInstance(assemblyScope)
    return pico
}

One subtle thing here... The application scope components are pretty straightforward, and the session scoped component is obvious. The Authenticator is a request scoped object though. It is dependent on the Session in the session container, and the UserDAO in the application container, so its deps can be met going up the chain. It could fit in the session container and still meet its deps as well. It is in the request, though, so that it is only instantiated for actions (or other requst scoped components) which actually require it. So, let's see it used:

package org.skife.gear

import org.skife.gear.service.Authenticator

class Login {

    // Components
    private auth

    // Form Properties
    login = ""
    pass = ""
    error =""

    Login(Authenticator authenticator) {
        this.auth = authenticator
    }

    attempt() {
        try {
            auth.authenticate(login, pass)
            return "home"
        }
        catch (ServiceException e) {
            error = e.cause.message
            return "input"
        }
    }
}

This action will be instantiated via a PicoContainer whose parent is set to the request scoped container. It will, therefore, receive the authenticator and can do its stuff. Sweet and simple =) Now, the authenticator could as esily be used as a service backend to a single-signon service, desktop app, etc. I used nanoweb here as it is a great for short-and-sweet type things (like this example) and has mostly replaced ruby/cgi for minimal web apps for me as it simply give syou so much for so little.

1 writebacks [/src/java] permanent link