I hardly need to discuss why reusing code is beneficial. Code reuse usually makes program development faster and reduces bugs. Once a piece of code is encapsulated and reused, it is necessary to check a very small piece of code to ensure the correctness of the program. If you only need to open and close the database connection in one place throughout the application, it is much easier to ensure that the connection is normal. But I'm sure you already know all of this.
There are two types of reuse codes, which I call reuse types:
The first type is functional reuse, which is the most common type of reuse. This is also a kind of mastery for most developers. That is, reuse a set of subsequent instructions to perform some operation.
The second type is context reuse, that is, different functions or operation codes are encapsulated between the same contexts, and the same context is encapsulated as reuse code (the context here refers to a series of the same operation instructions). Although it is becoming more popular in control reversal, it is not common. Moreover, context reuse is not explicitly described, so it is not used by the system like functional reuse. I hope you will change after reading this article.
Function reuse
Functional reuse is the most common type of reuse. It is a reuse of a set of instructions that execute some kind of operation. The following two methods are to read data from the 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()){ // Reuse code User user = new User(); user.setName (result.getString("name")); user.setEmail(result.getString("email")); users.add(user); // END reuse code} 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()){ // Reuse code User user = new User(); user.setName (result.getString("name")); user.setEmail(result.getString("email")); users.add(user); // END Reuse code} result.close(); statement.close(); return users; } catch(SQLException e){ //ignore for now } finally { //ignore for now }}For experienced developers, it may be possible to discover reusable code soon. The place where "reuse code" is commented in the above code is the same, so reuse can be encapsulated. These are the operations that read user records into user instances. These lines of code can be encapsulated into their own methods, for example:
// Encapsulate the same operation into the readUser method 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, call the readUser() method in the above two methods (the following example shows only the first method):
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 can also be hidden in its own class using the modifier private.
The above is about reuse of functions. Functional reuse is to encapsulate a set of instructions that perform specific operations through methods or classes to achieve the purpose of reuse.
Parameterized operations
Sometimes you want to reuse a set of operations, but these operations are not exactly the same anywhere you use. For example, readAllUsers() and readUsersOfStatus() methods both open a connection, prepare a statement, execute it, and loop through the result set. The only difference is that readUsersOfStatus() requires a parameter to be set on PreparedStatement. We can encapsulate all operations into a readUserList() method. As shown below:
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 }} Now we call readUserList(...) method from readAllUsers() and readUsersOfStatus() and give different operation parameters:
public List readAllUsers(){ return readUserList("select * from users", new String[]{});}public List readUsersWithStatus(String status){ return readUserList("select * from users", new String[]{status});}I believe you can find other better ways to implement reuse functions and parameterize them to make it easier to use.
Context reuse
Context reuse is slightly different from feature reuse. Context reuse is the reuse of a series of instructions, and various operations are always performed between these instructions. In other words, reuse statements before and after various behaviors. Therefore context reuse often leads to an inversion of control style classes. Context reuse is a very effective way to reuse exception handling, connection and transaction lifecycle management, flow iteration and shutdown, and many other common operational contexts.
Here are two methods that are done with 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); // Different characters = 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(); // Different if(inputStream == null) return; IOException exception = null; try{ int character = inputStream.read(); while(character != -1){ buffer.append((char) character); // Different characters = inputStream.read(); } return buffer.toString(); // Different} finally { try{ inputStream.close(); } catch (IOException e){ if(exception == null) throw e; } }}The two methods are different from the flow operation. But the context around these operations is the same. The context code iterates and closes the InputStream. In addition to the differences in using comment marks, the above code is its context code.
As shown above, the context involves exception handling and ensures that the stream is closed correctly after iteration. Writing such error handling and resource release code again and again is cumbersome and error-prone. Error handling and correct connection handling are more complex in JDBC transactions. It's obviously easier to write code once and reuse it anywhere.
Fortunately, the method of encapsulating the context is simple. Create a context class and put the public context into it. In the use of the context, different operation instructions are abstracted into the operation interface, and then each operation is encapsulated in the class that implements the operation interface (called the operation class here). You only need to insert an instance of the operation class into the context. It can be done by passing an instance of the operation class as a parameter to the constructor of the context object, or by passing an instance of the operation class as a parameter to the specific execution method of the context.
The following shows how to separate the above example into context and operational interface. StreamProcessor (operation interface) is passed as a parameter to the processStream() method of StreamProcessorContext.
// Stream Processing Plugin Interface Public interface StreamProcessor { public void process(int input);}// Stream Processing context class public class StreamProcessorContext{ // Instantiate the StreamProcessor operation interface and serve as a parameter 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 you can use the StreamProcessorContext class to print out the stream content like the following example:
FileInputStream inputStream = new FileInputStream("myFile");//Example of the operation through anonymous subclass implementation of the StreamProcessor interface new StreamProcessorContext().processStream(inputStream, new StreamProcessor(){ public void process(int input){ System.out.print((char) input); }});Or read the input stream content like this and add it to a character sequence:
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, do anything with the stream by inserting a different StreamProcessor interface implementation. Once the StreamProcessorContext is fully implemented, you will never have any trouble with unclosed streams.
Context reuse is very powerful and can be used in many other environments outside of stream processing. An obvious use case is to handle database connections and transactions correctly ( open - process - commit()/rollback() - close() ). Other use cases are NIO channel processing and thread synchronization in critical sections ( lock() - access shared resource - unlock() ). It can also convert checked exceptions from the API to unchecked exceptions.
When you look for code suitable for context reuse in your project, look for the following modes of operation:
When you find such a pattern, the regular operations before and after may achieve context reuse.
Context as template method
Sometimes you'll want to have multiple plugin points in the context. If the 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. The template method is a GOF design pattern. Basically, the template method divides an algorithm or protocol into a series of steps. A template method is usually implemented as a single base class and provides a method for each step in an algorithm or protocol. To customize any step, just create a class that extends the template method base class and override the method of the step you want to customize.
The following example is a JdbcContext implemented as a template method. Subclasses can override the opening and closing of connections to provide custom behavior. The processRecord(ResultSet result) method must always be overridden because it is abstract. This method provides operations that are not in the context and are different in different cases using JdbcContext. This example is not a perfect JdbcContext. It is only used to demonstrate how to use template methods when implementing context.
public abstract class JdbcContext { DataSource dataSource = null; // The constructor without parameters can be used for subclasses without DataSource to get 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(); } // ProcessRecord(ResultSet result) method 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(statement != null){ try{ statement.close(); } catch(SQLException e) { /* ignore */ } } if(connection != null){ closeConnection(connection); } } }}This is a subclass that extends JdbcContext to read the user list:
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 needs to get a connection from the connection pool and release it back to the connection pool after use, you can insert the connection by overriding openConnection() and closeConnection(Connection connection) methods.
Note how to rewrite the insert operation code through the method. The subclass of JdbcContext overrides the processRecord method to provide special record processing. In the StreamContext example, the operation code is encapsulated in a separate object and is provided as a method parameter. The object that implements the operation interface StreamProcessor is passed as a parameter to the processStreamContext class processStream(...) method.
You can use both techniques when implementing the context. The JdbcContext class can pass ConnectionOpener and ConnectionCloser objects that implement the operation interface as parameters to the execute method, or as parameters of the constructor. Personally, I prefer to use separate operation objects and operation interfaces for two reasons. First, it makes it easier to unit test the operation code alone; second, it makes the operation code reusable in multiple contexts. Of course, the operation code can also be used in multiple places in the code, but this is just an advantage. After all, here we are just trying to reuse the context, not reuse the operations.
Conclusion
Now you have seen two different ways to reuse code. Classic feature reuse and less common context reuse. Hopefully context reuse will be as common as feature reuse. Context reuse is a very useful way to abstract code from the underlying details of an API (such as JDBC, IO, or NIO API, etc.). Especially if the API contains resources that need to be managed (on and close, get and return, etc.).
persistence/ORM API, Mr.Persister uses context reuse to achieve automatic connection and transaction lifecycle management. This way the user will never have to worry about opening or closing the connection correctly, or committing or rolling back the transaction. Mr.Persister provides context in which users can insert their operations. These contexts are responsible for opening, closing, committing, and rolling back.
The popular Spring framework contains a lot of context reuse. For example Springs JDBC abstraction. Spring developers use it as a "control reversal." This is not the only control inversion type used by Spring frameworks. The core feature of Spring is dependency injection bean factories or "application contexts". Dependency injection is another type of control inversion.
The above is the function and context reuse of Java code that the editor introduced to you. I hope it will be helpful to you. If you have any questions, please leave me a message and the editor will reply to you in time. Thank you very much for your support to Wulin.com website!