I have mentioned before, in Scoped State, that I think NanoWar got the containers in webapps things right. I've been thinking about it more now, and there is something I want that I cannot do yet.
NanoWar works by having a hierarchy of containers, one at the application scope, one in each session whose parent is the application container, and one is created for each request with the parent as the appropriate session container.
This model allows for stateful components in the meaningful scopes -- if you have session state, it is stored in a component scoped to the session. Actions can be stored at any level -- stateless ones (a la Struts or SpringMVC) may be defined in the application level scope, one-off actions (a la WebWork) can be defined in the request scope container. Wizard style ones (if you don't have a concept of wizard scope) can be session scoped, and use a state machine to figure out what is next.
Where this falls apart, however, is that you can only traverse down the hierarchy. A component in the session container can see components in the application container, but not the request container. Components in the application container cannot see any other scopes. If you think about components as state holders this scoping makes sense. There is even an idiommatic workaround where, say, a request scope action would pass a session scope component as an argument to a call on an application scope service. Here it is, a bit more clearly:
# Defined in Request Container
class JobController
inject :current_user
inject :job_dao
def list_jobs
return job_dao.find_active_jobs_for(current_user)
end
end
#Stored in Session Container under :current_user
class User
attr_accessor :name, :id
end
#Stored in Application Container under :job_dao
class JobDao
def find_active_jobs_for(user)
... return jobs from somewhere ...
end
end
Forgive the ruby-esque pseudo-code when talking about Java stuff, it just makes for clearer code. The inject :component_id
basically handles the dependency injection bits. We have a singleton dao, a "session singleton" current user (ie, only one ever for a given application session), and the current request scoped action (JobController
). The action passes the session scope component to the application scope component. Perfectly reasonable in this use case.
Let's look at an alternate design though, which may be less reasonable in this particular case, but much more so in some examples we'll look at after we express the idea. Let's give the container a concept of a scope. The current crop all have this in very limited form now, where the scope is either "global" or "per request for it" (ie, singleton or not). Let's define contexts within the container, and allow components to reference across those contexts:
beans:
context:
id: application
bean:
id: job_dao
class: JobDao
autowire: true
bean:
id: user_dao
class: UserDao
autowire: true
context:
id: session
bean:
id: current_user
class: User
autowire: true
context:
id: request
bean:
id: job_controller
class: JobController
autowire: true
Now, all these components can be aware of each other, but across context boundaries only via proxies, as any given call to one must be in (from?) a given context, so you can have multiple instances of a context
So we could rework the components earlier defined to have the dependencies like:
# Defined in Request Context
class JobController
inject :job_dao
def list_jobs
return job_dao.find_active_jobs_for(current_user)
end
end
#Stored in Session Context under :current_user
class User
attr_accessor :name, :id
end
#Stored in Application Context under :job_dao
class JobDao
inject :current_user
def find_active_jobs_for(user)
... return jobs from somewhere using current_user ...
end
end
The list_jobs
call is made with three specific contexts, a given request context, a given session context, and a given application context. The JobDao
has a proxy to the current_user
component, which will always evaluate to the one in the current context.
Now, for the case here, listing active jobs for a user who happens to be the currently logged in user, this doesn't buy us a whole lot in exchange for complexity in the container.
So, here is a better example which focuses on a pretty common requirement:
# Stored in Application Context
def class UserDao
inject :current_user
inject :dbi
def find_user(criteria)
return dbi.open |h|
users = h.query "select * from users where #{criteria} " +
"and (manager_id = :user or user_id = :user)",
{:user => current_user.id}
return results_to_object_list(users)
end
end
end
Which allows for filtering the users the current user is allowed to see right at the dao layer. The UserDao
is dependent upon knowing who the current user is, it is a required component, but under the hierarchy it has no access to it, so it would have to be passed in. If it is passed in to every method call, that is a smell =)
Another great example comes in an audit service which hooks into lifecycle events (this is getting aspecty, but so is the concept):
# Stores in Application Context
class AuditService
inject :current_user
inject :current_transaction
inject :current_action
before :current_action, :any_invocation {
log.audit "Invoking user=#{current_user} action=#{current_action}"
}
around :current_transaction, :commit { |action|
log.audit "Committing in user=#{current_user}, " +
"action=#{current_action}, " +
"tx=#{current_transaction}"
begin
result = action.proceed
rescue Exception => e
log.audit "Failed Commit in user=#{current_user}, " +
"action=#{current_action}, " +
"tx=#{current_transaction}"
raise e
end
log.audit "Committed in user=#{current_user}, " +
"action=#{current_action}, " +
"tx=#{current_transaction}"
return result
}
end
Which does aspect-oriented type transaction and action audit logging. State is tracked in the components, bound to the context, but is available throughout the container, assuming there is a valid context in effect (think Spring transaction system, if you've looked into how it is implemented).
This certainly isn't the only way to solve these problems, but hey, it's an interesting one, and not a big step from what IoC containers are doing now.