The Source for Java Technology Collaboration
User: Password:



   

IoC Container Face-Off IoC Container Face-Off

by Ken Ramirez
02/10/2005

Contents
Terminology
The Problem Domain
The Service Locator Pattern
The IoC Pattern, Defined
Dependency Injection Types
PicoContainer
HiveMind
Some Final Thoughts

Within the web world, you won't find too many web applications that aren't built on top of the likes of Model View Controller, Business Delegate, Session Facade, Data Access Object, or other patterns these days. These patterns have been used to form architectures that attempt to provide a stronger foundation for our applications. By utilizing some of these patterns together, we avoid the problems faced by past development efforts, and provide extensibility for future growth. However, one problem still remains: component dependency resolution.

In this article, we will take a look at the problem in more depth and learn how others have tried to solve the problem by utilizing frameworks that implement the Inversion of Control (or IoC) pattern. First, we'll become familiar with some terms, the IoC pattern, and other patterns that have tried to implement a solution (but didn't completely succeed). Then we'll move on, to see how two of the most popular IoC frameworks are used today. These frameworks are PicoContainer and HiveMind.

Terminology

Throughout the remainder of this article, we'll refer to components, services, and classes interchangeably. In a more realistic setting, components could mean self-reliant pieces of code that are made up of various classes in order to provide a local reusable service. You could also say that the word "service" is used to refer to a remote component (as is the case with web services today).

I don't want you to become hung up on these terms while trying to understand the concepts you're about to meet, since these terms change from time to time to mean different things. Regardless of what you are trying to connect, the problem still remains, and hopefully, the solution will still hold as well.

In the examples you are about to see, the pieces we're trying to connect are simple classes, but they could be replaced with more complicated components, services, or objects. You'll see me refer to some of these classes as services or even components, but in the end, they are just simple classes used within the example to make the point stick.

Another term you will see is "bootstrap." This normally refers to a piece of code that connects various objects together prior to being used by the actual application or system.

Finally, you should become familiar with the term "container" (a term that has been overloaded tenfold) as used in this context. Here, we're referring to an object that holds other objects. This can be a simple component or class, or something more complicated, such as a framework.

The Problem Domain

In every application that you will ever have to build, there will be components that need to communicate with each other. For example, you might have to build a front end that communicates with the user and manages the presentation, navigation, and interaction. There may also be some middle-tier code that communicates with data stores, message services, or mainframes, and returns the retrieved data to the front-end code. Furthermore, the back-end pieces that actually communicate with the above mentioned subsystems will also have to be built and connected to the rest of the system.

Let's look at an example of this kind of programming in Java. First, let's set the stage. Imagine that we have a component named DomainStore (which implements the Domain Store pattern). The purpose of this component is to abstract the caller, who wishes to persist some data to a persistent data store. Keep in mind that our best practices dictate that the caller should not have to care where the final destination will be. The underlying DataStore should utilize an appropriate implementation for storing or loading the data.

The actual underlying storage could be a local file, a database, or even a message queue. The point is that the caller shouldn't have to be concerned about what the underlying data store will be. In the real world, we all know that at some point, someone will have to decide what the actual data store will be, but this can either be chosen dynamically, or through a configuration file.

Before we look at the DomainStore implementation, let's first look at the interface that the DomainStore will need to communicate with when attempting to pass data or read data from the underlying data store. The interface (as defined in Listing 1) implements the Data Access Object (or DAO) pattern we've come to know and love by now. There are the usual create, read, update, and delete methods (CRUD, as it's humbly come to be known as).

Listing 1. The DataAccessObject interface definition


package integrationtier;

public interface DataAccessObject {

    void create(Object dataObject);

    Object read(String criteria);

    void update(String criteria,
    	Object dataObject);

    void delete(String criteria);
}

The actual implementation for this interface could come in many different forms. It could be implemented as a database-aware object utilizing JDBC to communicate with the database. It could be defined to use files and streams to communicate with local files or files on a network. Or, it could be implemented using JMS to communicate with a message queue.

Listing 2 and Listing 3 provide two implementations. The first one provides a local file implementation and the second a database access implementation. Of course, I didn't create the actual code within the methods to do either, but you can tell by my comments what would actually be performed within those methods. Remember the point here: it shouldn't matter what the classes do to achieve their work, just that the implementation will be different. Given this fact, I could design the interface and provide one or two implementations of the interface, and then have others provide more implementations. Because the DomainStore uses the DataAccessObject interface to communicate with the data store, it doesn't care what the underlying implementation does.

Listing 2. The FileDataAccessObject class implementation


package integrationtier;

public class FileDataAccessObject implements
  DataAccessObject {

  public void create(Object dataObject) {
	// This implementation performs the
	// following steps:
	//  1. Open the file
	//  2. Go to the end of the file.
	//  3. Serialize the content of the data
	//     object to the file.
	//  4. Close the file.
  }

  public Object read(String criteria) {
	// This implementation performs the
	// following steps:
	//  1. Open the file.
	//  2. Continue to read data from the
	//     file until we reach the
	//     record we're looking for.
	//  3. Serialize in the record and reconstruct
	//     an object.
	//  4. Close the file
	//  5. Return the object to the caller.
	return null;
  }

  public void update(String criteria,
    Object dataObject) {
	// This implementation performs the following
	// steps:
	//  1. Open the file.
	//  2. Continue to read data from the file
	//     until we reach the
	//     record we're looking for. Use the
	//     criteria to find the record.
	//  3. Serialize out the record from the
	//     passed in object
	//     at that location.
	//  4. Close the file
  }

  public void delete(String criteria) {
	// This implementation performs the following
	// steps:
	//  1. Open the file.
	//  2. Read the entire content of the file.
	//  3. Serialize out the record from the
	//     passed in object
	//     at that location.
	//  4. Close the file
  }

}

Listing 3. The DbDataAccessObject class implementation


package integrationtier;

public class DbDataAccessObject implements
  DataAccessObject {

	public void create(Object dataObject) {
		// 1. Access a connection to the database
		// 2. Construct an SQL statement
		// 3. Execute the SQL statement using
		//    data from the DataObject.
		// 4. Close the database objects.
	}

	public Object read(String criteria) {
		// 1. Access a connection to the database
		// 2. Construct an SQL statement
		// 3. Execute the SQL statement (receive a
		//    resultset)
		// 4. Place the data from the result set
		//    into the DataObject.
		// 5. Close the database objects.
		return null;
	}

	public void update(String criteria,
	  Object dataObject) {
		// 1. Access a connection to the database
		// 2. Construct an SQL statement
		// 3. Execute the SQL statement using data
		//    from the DataObject
		//    to update the database record.
		// 4. Close the database objects.
	}

	public void delete(String criteria) {
		// 1. Access a connection to the database
		// 2. Construct an SQL statement
		// 3. Execute the SQL statement using data
		//    from the
		//    Criteria string to delete the
		//    respective record.
		// 4. Close the database objects.
	}

}

Listing 4. The DomainStore class implementation


package integrationtier;


/**
 * This class provides a Data Access service. It
 * is provided with the specific DataAccessObject
 * it should use when storing, retrieving,
 * deleting, or updating objects.
 */
public class DomainStore {
  private DataAccessObject dao = null;

  public DomainStore(DataAccessObject dao) {
		this.dao = dao;
  }

  /*
   * Store a new object or update existing
   * object using this method.
   */
  public void store(String criteria,
    Object dataObject, boolean isNew) {

	if(isNew)
		dao.create(dataObject);
	else
		dao.update(criteria, dataObject);
  }

  /*
   * Retrieve an existing object.
   */
  public Object load(String criteria) {
	return dao.read(criteria);
  }

  /*
   * Remove an existing object.
   */
  public void remove(String criteria) {
	dao.delete(criteria);
  }
}

Listing 4 provides the DomainStore implementation. The first thing to notice about this class is that it expects to receive a reference to an object, which implements the DataAcessObject interface. It doesn't care what the actual implementation does. It just knows that when its owner calls any of its methods, it will delegate the call to a DataAccessObject implementation, which will handle the actual call and do whatever it was built to do.

As you can see from the class definition, any decisions that need to be made prior to calling the underlying DataAccessObject are made within the DomainStore; for example, take a look at the store method. Also notice that the methods have slightly different names than the underlying DataAccessObject's methods. This was done in order to provide another level of abstraction, required in this case to abstract the client from the underlying data store.

Now, let's take a look at the client code that would be required in order to connect these pieces together and get some data passing back and forth. Please refer to Listing 5.

Listing 5. The client code that connects the components in this example


package dependentsolution;

import integrationtier.*;

/**
 * This class creates and uses the necessary
 * services directly, causing a Component
 * Dependency between the various pieces.
 */
public class DependentSolutionTest {

  public static void main(String[] args) {
	// Call the necessary business functionality.
	performBusinessTask();
  }

  /*
   * Performs business tasks - Imagine that this
   * functionality
   * is called from or via a Business Delegate.
   */
  private static void performBusinessTask() {
	// We're going to use the Database DAO
	// implementation.
	DataAccessObject dbDao =
	  new DbDataAccessObject();

	// Specify the Database DAO as its DataStore
	// device.
	DomainStore dbStore = new DomainStore(dbDao);

	// Tell the datastore to load an object named,
	// "Ken".
	Object person = dbStore.load("Ken");
  }
}

As can be seen in Listing 5, the business code has to literally connect the various pieces together by making a conscious decision that it wants to communicate with a database through a domain store. Remember that I said that at some point, something has to decide how these pieces are connected and which pieces are connected in order to accomplish the task at hand. Another problem here is that I have to either keep global objects around somewhere in my code for these components, or I have to continue to create them as I need them. This complicates matters even further, because if I ever need to change the code to use another type of underlying storage, I have to go through the code and refactor it.

This, of course, is a very simple example to demonstrate the problem. However, I've come across many applications that were written in this fashion. Later, when it was time to adopt a different component implementation, the project teams realized that there were one or more component dependencies, which they needed to resolve. This example makes use of interfaces and provides the functionality by implementing the interface. I've seen applications that created classes directly with no interface in between, which of course led to more refactoring when the time came to switch to a different implementation. Now that we've seen and hopefully understand the problem, let's look at one possible solution.

Pages: 1, 2, 3

Next Page » 

View all java.net Articles.

 Feed java.net RSS Feeds