Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 3 of 4
UML diagram of new abstract types. Click on thumbnail to view full-size image.
Using these new abstract types, the core logic becomes:
private final DataSource tradeDB =
DataSourceManager.get("tradeDB");
private static final Query getTradesByStatus =
new Query(
"getTradesByStatus",
"select id, account, transtype, symbol, quantity,
status " +
"from trades where status='{0}'");
public final void processPendingTrades()
throws QueryException
{
getTradesByStatus.setArguments(new String[]
{Trade.STATUS_PENDING});
ResultSet resultSet = null;
try
{
resultSet = tradeDB.getResultSet
(getTradesByStatus);
while (resultSet.next())
{
final String id = resultSet.getString(1);
final String account = resultSet.getString(2);
final String transType = resultSet.getString(3);
final String symbol = resultSet.getString(4);
final int quantity = resultSet.getInt(5);
final String status = resultSet.getString(6);
Trade trade =
new Trade(id, account, transType,
symbol, quantity, status);
process(trade);
}
}
finally
{
if (resultSet != null)
resultSet.dispose();
}
}
Other than the way we create and dispose of ResultSet, the logic remains identical. The Bridge pattern's nature allows the type implementation to vary without affecting that type's
clients. The types are specified as abstract base classes (or ABCs), which forward their calls on to the implemented subclass.
For example, here is the com.paulitech.query.ResultSet abstract class's next() method:
public final boolean next() throws QueryException
{
try
{
return nextImpl();
}
catch (Exception e)
{
throw new QueryException
("error getting next result", e);
}
}
protected abstract boolean nextImpl()
throws Exception;
When a client object asks a ResultSet instance to go to the next() result, the next() method in the ResultSet ABC actually does the executing. This method forwards the call to the nextImpl() method (short for next implementation), which the concrete subclass must implement. Any exception that the subclass throws
is then caught and repackaged in a com.paulitech.query.QueryException. This exception wrapping allows all implementations to adhere to a common interface. (For example, the implementation code
might throw a java.sql.SQLException. The base class should not know about such implementation-specific details). Notice that nextImpl() is declared abstract, which means that any concrete ResultSet subclass must implement it. Let's look at the standard SQL implementation in the class com.paulitech.query.jdbc.JDBCResultSetAdapter:
private java.sql.ResultSet resultSet;
protected boolean nextImpl() throws SQLException
{
return resultSet.next();
}
If this Bridge concept still isn't clear to you, spend time reviewing the full source code provided in the com.paulitech.query package (the abstract types used by client code) and the two different concrete implementations in com.paulitech.query.jdbc and com.paulitech.query.xml. When you experience the required "a-ha!" moment (make sure you are seated), you may proceed.
Note that the JDBC implementation of our abstract com.paulitech.query.ResultSet type actually contains an instance of the familiar java.sql.ResultSet, to which it forwards the messages it receives. This approach of wrapping one object in a different interface and forwarding
calls has been identified as the Adapter pattern. (Based on convention, you include the design pattern name in the type name
if using a well-known pattern; thus the name JDBCResultSetAdapter.)
Since most nontrivial applications access data from more than one source, the DataSourceManager object must manage multiple DataSources. DataSourceManager contains a simple mapping of String names (such as tradeSource) to instantiated DataSource objects. Pass DataSourceManager a String and it returns a DataSource that you may query.
Many code sections throughout an application need access to DataSources. How do we best provide access to the DataSourceManager? Should we pass a reference to the DataSourceManager to each method in the call chain? In addition to being tedious, that would bloat the code for methods only passing the reference
to those methods actually needing it. Should we pass the DataSourceManger upon object construction, and maintain a global reference to it? Object-oriented programming is supposed to save us from
globals -- and how do we know that all DataSourceManager references actually point to the same instance? If a bug created two different DataSourceManager objects, we'd pull out our hair trying to figure out the problem.
The Singleton design pattern, the standard approach to this common design problem, ensures that only one instance of a given
type is created, and provides a standard means of accessing that one instance. Here is a snippet of code from the DataSourceManager class, implemented as a Singleton:
private static DataSourceManager theInstance;
private Map dataSources;
private DataSourceManager()
{
dataSources = new java.util.HashMap();
}
private static synchronized DataSourceManager
getInstance()
{
if (theInstance == null)
theInstance = new DataSourceManager();
return theInstance;
}
public static void put(String label, DataSource
dataSource)
{
getInstance().dataSources.put(label, dataSource);
}
public static DataSource get(String label)
{
return (DataSource)(getInstance().dataSources.
get(label));
}
Since Java's static modifier associates a variable with the class itself rather than its instances, and only one Class object is loaded for this type, you can conveniently implement Singleton using a static member variable. When we set up a
new DataSource by calling DataSourceManager.put(), the put() method first calls getInstance() to get a handle to the current Singleton instance. Notice that getInstance() examines the static variable to see if the instance has initialized yet. If not (i.e., this is the first time anyone has
attempted to access the Singleton), the Singleton initializes, and then the value returns. Deferring initialization until
a client actually requires the object is known as lazy initialization, because it does no more work than necessary. If no clients need the Singleton, the initialization code never
executes, thus saving computation costs.
Now that we have an interface that performs in both live and test situations, a question remains: how and when do we specify
whether the situation is live or a test? This is another common problem in object-oriented design -- the Abstract Factory
design pattern to the rescue. A factory is an object that creates other objects. In our example, the DataSource object is a factory, since it creates ResultSet objects. Abstract Factory allows the object type to vary, depending on which factory type has instantiated. Thus, early in
the program, we instantiate with the label "tradeDB" either a JDBCDataSource object (passing the appropriate database, username, and password) or an XMLDataSource object (passing the appropriate XML file resource path).