September 3, 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.
About the author
Brian McCallister
Blog: http://kasparov.skife.org/blog/
Brian McCallister doesn't particularly like writing bios or writing about himself in the third person. He does love programming and systems work though, and tends to find himself doing a lot of both. Brian has also quite enjoyed giving presentations and seminars in the past, which isn't too hard as he loves teaching and exploring new ideas.
|