JicarillaWiki   IocContainerInternals/TheBasics/TheComponentizedContainer UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View

This wiki is in 'slumber' mode, just like its associated sourceforge project. Edits are disabled, the content is potentially stale and is not maintained. That said, it contains some really useful stuff still. Enjoy!


Contents | Previous | Next

The Componentized Container

(part of the IocContainerInternals paper)

Okay, so now that we've got the basic container idea right, let's start looking at how we might implement such a beast. We will (of course!) seperate our concerns and invert our control. I already identified some common concerns on the previous page.

Creation

Here's our first interface:

public interface Factory 
{ 
  Object newInstance(); 
} 

we saw that one before. The contract surrounding this interface is real simple: create a new instance of some component when newInstance() is called and return it. Exactly what some, that we will leave unspecified for now. Don't worry, we'll get to it.

Lookup

I think the [WWW]Map interface is a bit big, so we'll split it into several smaller contracts. The first one is looking components up from the container:

public interface Resolver 
{ 
  Object get( Object key ); 
  boolean provides( Object key ); 
  Object[] getKeys(); 
} 

this is a bit simpler than the lookup operations of the Map interface, but that's not a bad thing. There's a real simple contract: you get components from a Resolver by looking them up using a key. Simplicity is key here. But do note we suffer a little 'feature creep': we not only support lookup, but we also support two fundamental query operations ("can you resolve this key?" and "what keys can you resolve?").

That might seem trivial, but its not, actually. Consider what happens if you have a real complex distributed system with lots of computers providing different components, and those computers going on- and offline all the time. Your query to resolve a key may return true if a computer providing the component you need is online, but by the time you try and actually retrieve the component, the computer might have gone offline and the Resolver will not be able to comply. Tough luck. We'll probably need exceptions there, and some rather tight and formal contracts that make these kinds of things unambiguous. But let's not worry about that yet.

Population

The second bit of the Map interface is about adding things to the container. You can add any key/value pair to a map, but our container is less general. You can only add components:

public interface ModifiableContainer 
{ 
  void addComponent( Object key, Object component ); 
} 

okay, so that doesn't look much different from put(Object,Object). But its the thought that counts: the contract is simpler (because its more strict). You can only add components. That's it. Also note this is again simpler, because we don't have the equivalent of the putAll(Map) operation of the Map interface. I'll explain why later.

Modification?

And then there's the remove(Object) method. We're not going to support it on the first iteration of our container. Why not? Well, why should we? Experience in developing containers has shown us that the need to actually remove components from a container is quite rare. Most applications that you know today which are sort of container<->component-based don't support it, or only in an awkward way. Take your average IDE or editor as an example. Most of those support the addition of some kind of "plugin" for adding new functionality. The IDE is the container, the plugin is the component. But with most IDEs, you'll have to restart the application after adding or removing a plugin. This is fine, since you don't add or remove plugins that often.

Another thing to consider...in all your java development experience, how often have you encountered the need to unload a class? I thought so.

Initialization?

If you've been paying attention, you'll remember seeing a line of code container an container.initialize() line. Do you think we need one of those? An initialize() method is part of a complex component lifecycle. In the StoryOfThe6LifecycleMethods essay, I argue against using them, and argue against needing them as well. I've written several containers that don't need them, and several more exist out in the wild. Let's leave initialization out of the picture for now. "Simplify, dude!"

Putting things together

We now have three rather small interfaces, and rather small contracts surrounding them. Let's try and put them together:

public interface ContainerVersionOne 
{ 
  // contract #1: you can retrieve components from the container by key 
  Object get( Object key ); 
 
  // contract #1.5: you can figure out what components are inside the container 
  boolean provides( Object key ); 
  Object[] getKeys(); 
 
  // contract #2: you can add components to the container by key 
  void addComponent( Object key, Object component ); 
} 

hey! Wait a second! Where'd the Factory go? Seems we made a mistake somewhere already. A few pages back, I mentioned the container is responsible for all the new XXX() in your application. Whoops! Can you see the problem? Don't worry if you can't; this kind of thing took many smart people years to figure out (yes, really, and they still disagree). Try this:

  public interface ContainerVersionTwo 
  { 
    // contract #1: you can retrieve components from the container by key 
    Object get( Object key ); 
 
    // contract #1.5: you can figure out what components are inside the container 
    boolean provides( Object key ); 
    Object[] getKeys(); 
 
    // contract #2: you can add component ***factories*** to the container by key 
    void addComponent( Object key, Factory componentFactory ); 
  } 
we changed the second contract. In doing so, we've left the ability to compare the Container with a simple version of a Map. After all, with a Map, we could implement get() like this...

  Map m_backend = new HashMap(); 
 
  // ... 
 
  public Object get( Object key ) 
  { 
    return m_backend.get( key ); 
  } 

now, this would return a Factory. So the code changes to this...

  Map m_backend = new HashMap(); 
 
  // ... 
 
  public Object get( Object key ) 
  { 
    Factory factory = (Factory)m_backend.get( key ); 
    return factory.newInstance(); 
  } 

okay. That makes sense. Perfect. Right?

Holding our components

Wrong. Remember how we decided the container would be responsible for holding our objects? Well, its not holding on to them now! It's just holding on to their factories. The behaviour of the get() method above might be what we want for some applications, but what about components of which we only want a single instance to exist? What about supporting something like a singleton?

That's a tough nut, and most containers have different ideas about that. Some of them only support 'singletons', and some support none of them. Just as an example, here's how you could change the above code to use singletons...

  Map m_factories = new HashMap(); 
  Map m_instances = new HashMap(); 
 
  // ... 
 
  public Object get( Object key ) 
  { 
    if( m_instances.containsKey( key ) ) 
      return m_instances.get( key ); 
 
    Factory factory = (Factory)m_factories.get( key ); 
    Object instance = factory.newInstance(); 
    m_instances.put( key, instance ); 
    return instance; 
  } 

As before, these examples are some pretty bad code. There's no provision for checking for 'null', no exception handling, etc etc. But that's why its only examples.

Other caching setups

We've seen two extremes in component caching: not caching components at all on the one hand, and caching all components on the other hand. There's a whole range of possibilities in between. Common caching needs include

and you can probably imagine more. Clearly, moving the 'new' keyword is not as easy as it seemed. It seems really beneficial to be able to choose the caching setup the container uses.

But how much choice do we want? Here's some possible options:

  1. don't offer the choice in a single container implementation, but provide multiple implementations that have different caching setups
  2. offer the choice during container construction/configuration (perhaps an optional constructor parameter of some kind...)
  3. offer the choice on a per-component basis (perhaps an optional parameter to the addComponent() method)

Boy, what a tough choice! Again, each and every one of these options has actually been used in real container implementations. Option 1 seems the easiest to implement (we sketched two different, real simple, implementations already). Option 2 seems workable as well. Option 3 certainly seems the most flexible, but it does make the act of assembling the container more difficult.


I'm going to let you ponder that choice for a bit. The simpsons are on tonight!


Contents | Previous | Next
PythonPowered
EditText of this page (last modified 2007-02-18 14:14:22)
FindPage by browsing, title search , text search or an index
Or try one of these actions: AttachFile, LikePages, LocalSiteMap, SpellCheck

Creative Commons License
The contents of this wiki are licensed under a Creative Commons License.