 |
Don't Repeat Try Catch Blocks
The DontRepeatYourself principle can be applied in many circumstances.
One circumstance which the Java community seems to ignore is that of repetitive try-catch code. Like any other code repetition, this can be easily removed.
Example
This example applies to JDBC, but can be easily generalised to any case in which the same try-catch block is repeated throughout an application.
In JDBC code, code related to transactions is often repeated in every instance where a transaction is needed. Such code might look something like this:
fConnection = DbConnection.get();
try {
fConnection.setAutoCommit(fOFF);
//perform N SQL operations here
fConnection.commit();
fConnection.setAutoCommit(fON);
}
catch(Throwable ex){
fLogger.severe("ROLLING BACK TRANSACTION.");
try {
fConnection.rollback();
}
catch(SQLException ex){
throw new DAOException("Cannot rollback transaction.", ex);
}
throw new DAOException("Rolled back transaction: " + ex, ex);
}
finally {
DbUtil.logWarnings(fConnection);
DbUtil.close(fConnection);
}
return result;
The problem is that this code structure often gets repeated every time a transaction is required. The solution is to place the repeated code structure in an abstract base class which implements a "template method" design pattern.
Here is an example of one implementation.
1. An interface generic enough to apply to both local and distributed transactions:
package hirondelle.web4j.data;
/**
* Execute a database transaction.
*
* <P>Should be applied only to operations involving more than one SQL statement.
*
* @used.By {@link MyUserDAOSql}.
* @author <a href="http://www.javapractices.com/">javapractices.com</a>
*/
public interface Tx {
/**
* Execute a database transaction, and return the number of edited records.
*/
int executeTx() throws DAOException;
}
2. An abstract base class specifically for local transactions (a similar class can be defined for the case of distributed transactions as well):
package hirondelle.web4j.data;
import java.sql.*;
import java.util.logging.*;
/**
* Execute a local, non-distributed transaction versus a single database,
* using a single connection.
*
* <P>Do not use this class in the context of a
* <code>UserTransaction</code>.
*
* <P>This abstract base class implements the template method design pattern.
*
* @used.By {@link MyUserDAOSql}.
* @author <a href="http://www.javapractices.com/">javapractices.com</a>
*/
abstract class AbstractLocalTx {
/**
* Calls the abstract method {@link #executeMultipleSqls}, as part its template.
*/
public final int executeTx() throws DAOException {
int result = 0;
fLogger.fine("Editing within a local transaction.");
fConnection = DbConnection.get();
try {
startTx();
result = executeMultipleSqls(fConnection);
endTx();
}
catch(Throwable ex){
rollbackTx();
throw new DAOException("Rolled back transaction: " + ex, ex);
}
finally {
DbUtil.logWarnings(fConnection);
DbUtil.close(fConnection);
}
fLogger.fine("Total number of edited records: " + result);
return result;
}
/**
* Execute multiple SQL operations in a single local transaction.
*
* <P><em>Design Note</em>: allowing only <code>SQLException</code> in the
* <code>throws</code> clause simplifies the implementor significantly, since no
* <code>try-catch</code> blocks are needed. Thus, the caller has simple,
* "straight-line" code.
*
* @param aConnection must be used by all SQL statements participating in
* this transaction
* @return number of records edited by this operation.
*/
abstract int executeMultipleSqls(Connection aConnection) throws SQLException;
// PRIVATE //
/**
* The connection through which all SQL statements attached to this
* transaction are executed.
*/
private Connection fConnection;
private static final boolean fOFF = false;
private static final boolean fON = true;
private static final Logger fLogger =
Logger.getLogger(AbstractLocalTx.class.getPackage().getName());
private void startTx() throws SQLException {
fConnection.setAutoCommit(fOFF);
}
private void endTx() throws SQLException {
fConnection.commit();
fConnection.setAutoCommit(fON);
}
private void rollbackTx() throws DAOException {
fLogger.severe("ROLLING BACK TRANSACTION.");
try {
fConnection.rollback();
}
catch(SQLException ex){
throw new DAOException("Cannot rollback transaction.", ex);
}
}
}
3. The typical user of AbstractLocalTx? looks like this :
public int delete(String aLoginName) throws DAOException {
Tx deleteUser = new DeleteUserTx(aLoginName);
return deleteUser.executeTx();
}
//...
private class DeleteUserTx extends AbstractLocalTx {
DeleteUserTx(String aLoginName){
fLoginName = aLoginName;
}
int executeMultipleSqls(Connection aConnection) throws SQLException {
//perform N SQL operations, using aConnection
//NO TRY-CATCH IS USED, since SQLException may be thrown!!
}
private final String fLoginName;
}
The benefits of such a technique are
- the caller is shorter
- the caller is much simpler, since the code is "straight-line", and contains no try-catch blocks
- smaller overall code base
- handling of transactions is defined in one class
- much easier maintenance
The above examples are taken with permission from web4j, which is a simple, effective web application framework.
A bit of a rant:
The repetition of try-catch blocks (along with other items) seems to be often blamed for making JDBC code difficult to work with. It is often insinuated in such contexts that "JDBC code is complex."
To quote Ulysses, I am "rather inclined to pooh-pooh the suggestion as egregious balderdash."
JDBC code is not inherently difficult or complex. It's just another API, no more difficult to work with than any other. I would challenge those who assert that "JDBC is complex" to demonstrate the fact using well-factored code, instead of pointing to code of dubious quality which is riddled with repetition.
-- JohnOHanley - 20 Oct 2003
|