|
|
||||||||||||||||
by Ken Ramirez | ||||||||||||||||
| |||||||||
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.
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.
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.
View all java.net Articles.
|
|