Brian's Waste of Time

Tue, 08 Mar 2005

Containers and Webapps

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.

NanoWar Container Hierarchy

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:

Logical 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

Deployed Contexts

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.

0 writebacks [/src/java] permanent link