Code Reuse: Action and Context Reuse
Jakob Jenkov |
I hardly need to argue why reusing code is beneficial. Code reuse (usually) leads to faster development and less bugs. Once a piece of code is encapsulated and reused, there is less code to check for correctness. If database connections are only opened and closed in a single place throughout your application it is a lot easier to assure that the connection handling works correctly. But I'm sure you already know all this.
There are two types of code reuse. I call these reuse types:
The first type, action reuse, is the most commonly seen type of reuse. It is also the one most developers are taught in school or university. It is the reuse of a set of subsequent instructions that carry out a certain action.
The second type, context reuse, is the reuse of a common context (set of instructions) inbetween which different actions take place. In other words it is the reuse of code surrounding some other code. Context reuse is less commonly seen although it is gaining popularity with the inversion of control movement. Yet context reuse has not been described explicitly and is thus not being used as systematically as action reuse. That, I hope, will change after you read this text.
Action Reuse
Action reuse is the most commonly seen type of reuse. It is the reuse of a set of instructions that carry out a certain action. These two methods both read user records from a database:
public List readAllUsers(){ Connection connection = null; String sql = "select * from users"; List users = new ArrayList(); try{ connection = openConnection(); PreparedStatement statement = connection.prepareStatement(sql); ResultSet result = statement.executeQuery(); while(result.next()){ User user = new User(); user.setName (result.getString("name")); user.setEmail(result.getString("email")); users.add(user); } result.close(); statement.close(); return users; } catch(SQLException e){ //ignore for now } finally { //ignore for now } }
public List readUsersOfStatus(String status){ Connection connection = null; String sql = "select * from users where status = ?"; List users = new ArrayList(); try{ connection = openConnection(); PreparedStatement statement = connection.prepareStatement(sql); statement.setString(1, status); ResultSet result = statement.executeQuery(); while(result.next()){ User user = new User(); user.setName (result.getString("name")); user.setEmail(result.getString("email")); users.add(user); } result.close(); statement.close(); return users; } catch(SQLException e){ //ignore for now } finally { //ignore for now } }
The experienced developer can quickly see that the two blocks of code marked in bold are identical and thus can be encapsulated and reused. These lines carry out the action of reading a user record into a User instance. These lines can be moved to their own method, like this:
private User readUser(ResultSet result) throws SQLException { User user = new User(); user.setName (result.getString("name")); user.setEmail(result.getString("email")); users.add(user); return user; }
Now the readUser method can be called from both of the above methods like this (only the first method is shown):
public List readAllUsers(){ Connection connection = null; String sql = "select * from users"; List users = new ArrayList(); try{ connection = openConnection(); PreparedStatement statement = connection.prepareStatement(sql); ResultSet result = statement.executeQuery(); while(result.next()){ users.add(readUser(result)) } result.close(); statement.close(); return users; } catch(SQLException e){ //ignore for now } finally { //ignore for now } }
The readUser() method could also have been isolated in its own class instead of a private method.
That is what there is to say about action reuse. Action reuse is when you reuse a set of instructions that carry out a certain action. The instructions are typically reused by encapsulating them in a method or a class.
Parameterized Actions
Sometimes you want to reuse a little bit more of a set of instructions, even if the instructions are not exactly the same everywhere they are used. For instance, the readAllUsers() and readUsersOfStatus() methods both open a connection, prepare a statement, executes it, and iterates the ResultSet. The only difference is that the readUsersOfStatus() sets a parameter on the PreparedStatement. The two methods could be merged into one readUserList() method like this:
private List readUserList(String sql, String[] parameters){ Connection connection = null; List users = new ArrayList(); try{ connection = openConnection(); PreparedStatement statement = connection.prepareStatement(sql); for(int i=0; i < parameters.length; i++){ statement.setString(i, parameters[i]); } ResultSet result = statement.executeQuery(); while(result.next()){ users.add(readUser(result)) } result.close(); statement.close(); return users; } catch(SQLException e){ //ignore for now } finally { //ignore for now } }
This method could now be called from the readAllUsers() and readUsersOfStatus() like this:
public List readAllUsers(){ return readUserList("select * from users", new String[]{}); } public List readUsersWithStatus(String status){ return readUserList("select * from users", new String[]{status}); }
I am sure you can figure out many other or smarter ways to reuse actions and to parameterize them to be more reusable.
Context Reuse
Context reuse works slightly different than action reuse. Context reuse is the reuse of a set of instructions that various different actions always takes place inbetween. In other words, the reuse of statements before and after the various different actions. Therefore context reuse usually leads to inversion of control style classes. Context reuse is a really effective way to reuse exception handling, connection and transaction life cycle management, stream iteration and closing, and a lot of other common action contexts.
Here are two methods that both do something with an InputStream.
public void printStream(InputStream inputStream) throws IOException { if(inputStream == null) return; IOException exception = null; try{ int character = inputStream.read(); while(character != -1){ System.out.print((char) character); character = inputStream.read(); } } finally { try{ inputStream.close(); } catch (IOException e){ if(exception == null) throw e; } } } public String readStream(InputStream inputStream) throws IOException { StringBuffer buffer = new StringBuffer(); if(inputStream == null) return; IOException exception = null; try{ int character = inputStream.read(); while(character != -1){ buffer.append((char) character); character = inputStream.read(); } return buffer.toString(); } finally { try{ inputStream.close(); } catch (IOException e){ if(exception == null) throw e; } } }
The actions the two methods to with the streams are different. But the context around these actions is the same. The context is iterating and closing an InputStream. The context code is marked in bold in the code examples.
As you can see the context involves exception handling and guaranteed correct closing of the stream after iteration. Writing such error handling and resource releasing time and again is tedious and error prone. Error handling and correct connection handling is even more complicated in JDBC transactions. It would be easier to write this once and reuse it everywhere.
Fortunately the recipe for encapsulating contexts is simple. The context itself is put into its own class. The action, which is what differs between uses of the context, is abstracted away behind an action interface. Each action is then encapsulated in its own class which implements the action interface. A context then needs an action instance plugged in. This can be done by passing the action instance as parameter to the context object constructor, or by passing the action instance as parameter to the context's execute method.
Below is shown how the above examples can be separated into a context plus actions. A StreamProcessor (action) is passed as parameter to the StreamProcessorContext's processStream() method.
//the plugin interface public interface StreamProcessor { public void process(int input); } //the stream processing context class public class StreamProcessorContext{ public void processStream(InputStream inputStream, StreamProcessor processor) throws IOException { if(inputStream == null) return; IOException exception = null; try{ int character = inputStream.read(); while(character != -1){ processor.process(character); character = inputStream.read(); } } finally { try{ inputStream.close(); } catch (IOException e){ if(exception == null) throw e; throw exception; } } } }
Now the StreamProcessorContext class can be used to print out the stream contents like this:
FileInputStream inputStream = new FileInputStream("myFile"); new StreamProcessorContext() .processStream(inputStream, new StreamProcessor(){ public void process(int input){ System.out.print((char) input); } });
... or to read the stream input into a String like this:
public class StreamToStringReader implements StreamProcessor{ private StringBuffer buffer = new StringBuffer(); public StringBuffer getBuffer(){ return this.buffer; } public void process(int input){ this.buffer.append((char) input); } } FileInputStream inputStream = new FileInputStream("myFile"); StreamToStringReader reader = new StreamToStringReader(); new StreamProcessorContext().processStream(inputStream, reader); //do something with input from stream. reader.getBuffer();
As you can see you can do pretty much anything with the stream by plugging in different StreamProcessor implementations. Once the StreamProcessorContext is fully implemented you'll never have sleepless nights about unclosed streams.
Context reuse is quite powerful and can be used in many other contexts than stream processing. An obvious use case is the correct handling of database connections and transactions (open - process - commit()/rollback() - close()). Other use cases are NIO channel processing, and thread synchronization in critical sections (lock() - access shared resource - unlock()). It could also just be something as simple as turning checked exceptions of an API into unchecked exceptions.
When looking for code in your own projects that are suitable for context reuse, look for this pattern of actions:
- general action before
- special action
- general action after
When you find such patterns the general action before and after are possible candidates for context reuse.
Contexts as Template Methods
Sometimes you would like more than one plugin point in a context. If a context consists of many smaller steps and you want each step of the context to be customizable, you can implement the context as a Template Method. Template Method is a GOF design pattern. Basically Template Method splits an algorithm or protocol into a sequence of steps. A Template Method is typically implemented as a single base class with a method for each step in the algorithm or protocol. To customize any of the steps simply create a class that extends the Template Method base class and override the method of the step you want to customize.
The example below is a JdbcContext implemented as a Template Method. The opening and closing of the connection can be overridden by subclasses to provide custom behaviour. The methods processRecord(ResultSet result) must always be overridden as it is abstract. This method provides the behaviour that is not part of the context. The behaviour that is different in each situation in which the JdbcContext is used. This example is not a perfect JdbcContext. It is only meant to show how to use a Template Method when implementing contexts.
public abstract class JdbcContext { DataSource dataSource = null; /* no-arg constructor can be useful for subclasses that do not need a DataSource to obtain a connection*/ public JdbcContext() { } public JdbcContext(DataSource dataSource){ this.dataSource = dataSource; } protected Connection openConnection() throws SQLException{ return dataSource.getConnection(); } protected void closeConnection(Connection connection) throws SQLException{ connection.close(); } protected abstract processRecord(ResultSet result) throws SQLException ; public void execute(String sql, Object[] parameters) throws SQLException { Connection connection = null; PreparedStatement statement = null; ResultSet result = null; try{ connection = openConnection(); statement = connection.prepareStatement(sql); for(int i=0; i < parameters.length; i++){ statement.setObject(i, parameters[i]); } result = statement.executeQuery(); while(result.next()){ processRecord(result); } } finally { if(result != null){ try{ result.close(); } catch(SQLException e) { /* ignore */ } } if(statement != null){ try{ statement.close(); } catch(SQLException e) { /* ignore */ } } if(connection != null){ closeConnection(connection); } } } }
Here is a subclass that extends the JdbcContext to read a list of users.
public class ReadUsers extends JdbcContext{ List users = new ArrayList(); public ReadUsers(DataSource dataSource){ super(dataSource); } public List getUsers() { return this.users; } protected void processRecord(ResultSet result){ User user = new User(); user.setName (result.getString("name")); user.setEmail(result.getString("email")); users.add(user); } }
Here is how to use the ReadUsers class.
ReadUsers readUsers = new ReadUsers(dataSource); readUsers.execute("select * from users", new Object[0]); List users = readUsers.getUsers();
If the ReadUsers class needed to obtain a connection from a connection pool and release it back to that connection pool after use, this could have been plugged in by overriding the openConnection() and closeConnection(Connection connection) methods.
Notice how the action code is plugged in via method overriding. Subclasses of JdbcContext override the processRecord method to provide special record processing. In the StreamContext example the action code was encapsulated in a separate object and provided as a method parameter. An object implementing an action interface, the StreamProcessor, was passed to the StreamContext's processStream(...) method as parameter.
You can use both techniques when implementing contexts. The JdbcContext class could also have been implemented to take an ConnectionOpener and a ConnectionCloser object as parameters to the execute method, or as parameters to the constructor. Personally I prefer using the separate action object and an action interface for two reasons. First, it makes the action code easier to unit test separately. Second, it makes the action code reusable across multiple contexts. This is of course only an advantage if the action is used in more than one place in the code. After all, it is the context we are trying to reuse here, not the action. You never know, however, if the action may one day be useful inside a different context. Software maintenance has a way of pulling code in directions it was never intended to go in the first place.
Closing Notes
You have now seen two different ways to reuse code. The classic action reuse and the less commonly seen context reuse. Hopefully context reuse will become as common as action reuse. Context reuse can be a really useful way to abstract your code from the lower details of API's like JDBC, the IO or NIO API's and others. Especially if the API contains resources that need to be managed (opened and closed, obtained and given back etc.).
Our persistence/ORM API, Mr. Persister, takes advantage of context reuse to implement automatic connection and transaction life cycle management. That way users will never have to worry about correctly opening or closing connections, or committing or rolling back transactions. Mr. Persister provides contexts that the users can plug their actions into. These contexts take care of the opening, closing, committing and rolling back.
The popular Spring framework contains lots of context reuse. For instance Springs JDBC abstraction. The Spring developers refer to their use of context reuse as "inversion of control". This is not the only type of inversion of control used by the Spring framework. A central feature of Spring is the dependency injection bean factories or "application contexts". Dependency injection is another type of inversion of control.
In his article about dependency injection Martin Fowler states that he prefers to call the various types of inversion of control by names that signal what they actually do. The inversion of control style that injects beans into other beans at creation time is what he calls dependency injection. In the same fashion I think we should refer to the inversion of control style that reuses a code context as context reuse, and not just "inversion of control".
Tweet | |
Jakob Jenkov |