| IocContainerInternals/TheBasics/CommonPatterns |
UserPreferences |
| JicarillaWiki | FrontPage | Overview | Articles | GetStarted | Sourceforge Project Page | RecentChanges |
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!
(part of the IocContainerInternals paper)
By now we've seen quite a few of the basic concepts that exist, in some form, inside just about every IoC container. We've seen some sample interfaces and some sample code that provide hints at possible ways to model a container. Let's take a look at some of the patterns and choices that are used in containers. These are not really design patterns, but they are about architecture.
Remember the factory interface?
public interface Factory
{
Object newInstance();
}
I showed a rather trivial implementation of this factory that wasn't very reusable earlier on. Here's a modified version:
class JButtonFactory implements Factory
{
Object getInstance()
{
return new JButton();
}
}
you can only use this for JButton instances. It would be quite boring to write such a factory for each and every component you use. We want to write a smart factory implementation once, so we can reuse it. How do we do that?
Well, a common strategy is to put some requirement on the component. Like:
Components must have a public no-argument constructor.
We can create a factory that knows how to create any component that fulfills that contract. Something like...
class BasicFactory implements Factory
{
Class classToCreateInstancesFrom;
BasicFactory( Class clazz )
{
classToCreateInstancesFrom = clazz;
}
Object getInstance()
throws Exception
// hey! Exception handling to figure out here!
{
return classToCreateInstancesFrom.newInstance();
}
}
note that the Class.newInstance() method throws all kinds of exceptions. To make this work, we'd need to add all kinds of assertions, validate arguments, wrap and unwrap exceptions, etc. Let's agree to ignore exception handling for now. Just remember that these code samples won't compile because they don't handle exceptions well.
Most real-life components don't work well with just a public no-argument constructor. Take javabeans as an example. Besides the constructor contract, they also have setXXX() methods which sometimes need to be called. The contract is a little like:
Components sometimes have public void methods whose names start with set, with the fourth letter being capital. These methods may need to be called before you can use the component, with appropriate arguments.
This is quite a weak contract (what's with the "may" and the "appropriate"?), but its well-known and used a lot. The contract is usually made stronger for each and every individual component by specifying it in documentation and unit tests.
Can we create a factory that can instantiate any javabean? Sure! Here's some sketchy code that shows the basics:
class BeanFactory implements Factory
{
Class classToCreateInstancesFrom;
Resolver resolverToGetPropertiesFrom;
BasicFactory( Class clazz, Resolver resolver )
{
classToCreateInstancesFrom = clazz;
resolverToGetPropertiesFrom = resolver;
}
Object getInstance()
{
Object instance = classToCreateInstancesFrom.newInstance();
PropertyDescriptor[] properties = Introspector
.getBeanInfo( classToCreateInstancesFrom )
.getPropertyDescriptors();
for( property : properties )
{
String name = property.getName();
Method setMethodToCall = descriptor.getWriteMethod();
Object methodArgument = resolverToGetPropertiesFrom.get( name );
setMethodToCall.invoke( instance, new Object[] { methodArgument } );
}
}
}
you can see us using the javax.beans package here, and some reflection magic. A robust and flexible bean factory has a lot more code to it than this, but you'll get the basic idea.
I picked the javabeans contract because its a well-known example of a component contract. There's many others. You'll hear people refer to dependency injection (in constructor and method variants), "type 1", "type 2" and "type 3" IoC, avalon-framework, XXXAware interfaces, and more. There's also JMX Beans, JNDI-querying beans, POJOs, EJBs, servlets, and more. It would be really boring to spend a lot of time talking about these contracts, their pros and cons. From the perspective of implementing a container, it's just a lot more of the same thing! You might imagine having a factory for javabeans, a factory for type 3 IoC components, a factory for servlets, etc etc. Rather boring bits of plumbing :-D
I wrote down a big list of "caching setups" on the previous page. You could choose to have a system full of singleton-like objects, or you could create new objects every time one is needed, or something else alltogether.
Most containers either support one type of caching. Contracts are often fairly rigid and simple. Stuff like:
A single instance of each component will live inside the container, and it will be shared between all the other objects in the system. The component probably needs to be thread safe and know how to handle multiple clients.
Other containers support a few types of caching. Their designers will take a good look at a particular set of problems, and will offer you only a few choices, that will work well for most solutions to those kinds of problems. Most frameworks designed for working inside a servlet engine are good examples. They'll offer a few types of caching, usually called "scopes". You can, for example, configure a component to be "shared" (between webapps), "singleton" (one component per webapp), or "per-session" (one per client session).
A third choice, which is currently a lot less common (but one I like myself :-D) is to support every kind of caching. The only way to feasibly do that without getting a very ugly and cluttered codebase, is to encapsulate the caching choice into a truly seperate concern (which it really seems to be) and create an abstraction for it. Besides containers, resolvers, and factories, we'll need to have a "Cacher" object of some kind. It probably needs to sit somewhere between the container and the factory. A typical setup might result in something like this:
Most containers today merge the whole caching concept with other concerns, so you won't typically see diagrams like this, nor will you easily recognize these abstractions in their code. I think that's a little ugly conceptually, but its working well for most of them in practice.
We've now identified several key concepts, even writing up basic interfaces for them. It should come as no surprise to you that most of these interfaces are not actually in use in actual containers. Besides being naive, these interfaces are also rather "small". Most containers merge a few concepts. Let's look at some common choices...
If you only decide to support a single type of component (for example, only javabeans) and just one or two types of caching (for example, only "singleton" and "per-request"), you can do away with distinct factory objects and distinct caching helpers. The container just handles instantiation and caching directly, probably using some static or private utility methods.
This usually results in a system that's less adaptable to changing circumstances using composition. Instead, you're likely to see class size increase over time, and you'll see a (sometimes complex) class hierarchy of container implementations as new features are added. For example, you might add support for pooling components by overriding a method or two in a subclass.
In the examples so far, we've made it appear as if the user would actually be calling methods on the container. With quite a few containers, things don't work that way. The container is locked down quite a bit. Instead of the container user being responsible for the assembly, the container user declares what it is that he wants assembled, usually in one or more xml configuration file. The container will read the config files and from there call the assembly methods on itself.
This results in a system that has a much more rigid seperation between the container and the container-using code. The container is given much more control. This hides a lot of complexity from the container user (you don't need to any "embedding", you just write a config file and start a program) and reduces the likelyhood of misconfiguration. The main disadvantage of a system like this is that its often more difficult to unit test, and more difficult to develop.
I haven't talked about things like classloading, jar management, classpath management, configuration management, logging, instrumentation, or transaction support. None of these things have much to do with the "core concept" of inversion of control. Yet there are several inversion of control solutions out there that come with a lot of support for these things. If the container is also the "embeddor", and in strong control of the environment, use of some of these features may be mandatory (for example, you'll be required to provide your application to the container in some kind of archive format).
Some IoC containers are built with a specific type of application in mind, and they enforce a certain application structure for those applications. For example, the container may handle request/response logic for its components, and it will enforce a contract on the components to fit into a piece of the request/response program flow. The servlet api is probably the best example of this kind of control. Various other web application solutions, often built on top of the servlet api, do similar things. Struts is an example of a container optimized for a certain kind of application. It enforces a particular kind of the Model-View-Controller architecture.
With these containers, their support for this kind of application flow is often mentioned as their dominant characteristic. The fact that they are actually also an IoC container will be somewhat hidden. A lot of the confusion in switching IoC containers is when people expect containers to enable the application flow they're used to.
Another common pattern that's a little different from concept merging is that of concept hiding. A container may internally have a really granularly componentized setup, but hide this from the user in the form of a simple, single facade. This means the average user does not need to be aware of all the concepts introduced (lookup mechanisms, factories, caching policies, ...), but instead focusses on getting the assembly done. The disadvantages of using facades is that they'll usually will make some complex setups difficult. The best facades provide a way to "look behind the curtain".
Frontends are known by different names, and their setups are often a little different. The term "facade" is not actually in use in containers as far as I know. Instead, there will be a Manager, Builder, Embeddor or Assembler. Some containers use, descriptively, Frontend :-D. The role of these different facades is usually much the same.
When trying to figure out how a container works, I usually recommend that people look at what the front end does, and try to do that themselves "manually" by interacting with the things behind the curtain in code they write themselves. This is because the call stack and behaviour of these frontends often seems to be a large part of what makes containers complex, especially if the containers provide lots of features that go above and beyond "basic IoC".