Date: 2004-03-05T20:37:24
Editor: 130.89.169.128 <>
Wiki: JicarillaWiki
Page: http://lsd.student.utwente.nl/jicarilla/IocContainerInternals_2fTheBasics_2fTheComponentizedContainer
no comment
New Page:
[wiki:IocContainerInternals Contents] | [wiki:IocContainerInternals/TheBasics/TheSmartHashMap Previous] | [wiki:IocContainerInternals/TheBasics/CommonPatterns 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!) seperating our concerns and invert our control. I already identified some common concerns on the [wiki:IocContainerInternals/TheBasics/TheSmartHashMap 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 [http://java.sun.com/j2se/1.4.2/docs/api/java/util/Map.html 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
* one component instance per thread
* one component instance per client
* one component instance per connection
* one component instance per request/response
* one component instance per transaction
* one component instance per session
* one component instance per classloader
* one component instance per JVM
* one component instance per distributed network
* a pool of component instances per '''pick-one-of-the-above'''
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
1. offer the choice during container construction/configuration (perhaps an optional constructor parameter of some kind...)
1. 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!
----
[wiki:IocContainerInternals Contents] | [wiki:IocContainerInternals/TheBasics/TheSmartHashMap Previous] | [wiki:IocContainerInternals/TheBasics/CommonPatterns Next]
-------------------------------------------------------
This SF.Net email is sponsored by: IBM Linux Tutorials
Free Linux tutorial presented by Daniel Robbins, President and CEO of
GenToo technologies. Learn everything from fundamentals to system
administration.http://ads.osdn.com/?ad_id=1470&alloc_id=3638&op=click
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.