转载

Java 代码重用:功能与上下文重用

我几乎不需要讨论为什么重用代码是有利的。代码重用通常使得程序开发更加快速,并使得 BUG 减少。一旦一段代码被封装和重用,那么只需要检查很少的一段代码即可确保程序的正确性。如果在整个应用程序中只需要在一个地方打开和关闭数据库连接,那么确保连接是否正常则容易的多。但我确信这些你已经都知道了。

有两种类型的重用代码,我称它们为重用类型:

  • 功能重用(Action Reuse)
  • 上下文重用(Context Reuse)

第一种类型是功能重用,这是最常见的一种重用类型。这也是大多数开发人员掌握的一种。即重用一组后续指令来执行某种操作。

第二种类型是上下文重用,即不同功能或操作代码在相同上下文之间,将相同上下文封装为重用代码(这里的上下文指的是一系列相同的操作指令)。虽然它在控制反转中越来越受欢迎但它并不常见。而且,上下文重用并没有被明确的描述,因此它并没有像功能重用一样被系统的使用。我希望你看完这篇文章之后会有所改变。

功能重用

功能重用是最常见的重用类型。它是一组执行某种操作指令的重用。下面两个方法都是从数据库中读取数据:

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);
            // END 重用代码
        }
        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);
            // END 重用代码
        }
        result.close();
        statement.close();
        return users;
    }
    catch(SQLException e){
        //ignore for now
    }
    finally {
        //ignore for now
    }
}

对于有经验的开发人员来说,可能很快就能发现可以重用的代码。上面代码中注释“重用代码”的地方是相同的,因此可以封装重用。这些是将用户记录读入用户实例的操作。可以将这些行代码封装到他们自己的方法中,例如:

// 将相同操作封装到 readUser 方法中
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;  
}

现在,在上述两种方法中调用 readUser() 方法(下面示例只显示第一个方法):

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
    }
}

readUser() 方法也可以在它自己的类中使用修饰符 private 隐藏。

以上就是关于功能重用的内容。功能重用是将一组执行特定操作的指令通过方法或类封装它们来达到重用的目的。

参数化操作

有时,你希望重用一组操作,但是这些操作在使用的任何地方都不完全相同。例如 readAllUsers()readUsersOfStatus() 方法都是打开一个连接,准备一条语句,执行它,并循环访问结果集。唯一的区别是 readUsersOfStatus() 需要在 PreparedStatement 上设置一个参数。我们可以将所有操作封装到一个 readUserList() 方法。如下所示:

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
    }
}

现在我们从 readAllUsers()readUsersOfStatus() 调用 readUserList(...) 方法,并给定不同的操作参数:

public List readAllUsers(){
   return readUserList("select * from users", new String[]{});
}

public List readUsersWithStatus(String status){
   return readUserList("select * from users", new String[]{status});
}

我相信你可以找出其他更好的办法来实现重用功能,并将他们参数化使得更加好用。

上下文重用

上下文重用与功能重用略有不同。上下文重用是一系列指令的重用,各种不同的操作总是在这些指令之间进行。换句话说,重复使用各种不同行为之前和之后的语句。因此上下文重用通常会导致控制风格类的反转。上下文重用是重用异常处理,连接和事务生命周期管理,流迭代和关闭以及许多其他常见操作上下文的非常有效的方法。

这里有两个方法都是用 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;
        }
    }
}

两种方法与流的操作是不同的。但围绕这些操作的上下文是相同的。上下文代码迭代并关闭 InputStream。上述代码中除了使用注释标记的不同之处外都是其上下文代码。

如上所示,上下文涉及到异常处理,并保证在迭代后正确关闭流。一次又一次的编写这样的错误处理和资源释放代码是很繁琐且容易出错的。错误处理和正确的连接处理在 JDBC 事务中更加复杂。编写一次代码并在任何地方重复使用显然会比较容易。

幸运的是,封装上下文的方法很简单。 创建一个上下文类,并将公共上下文放入其中。 在上下文的使用中,将不同的操作指令抽象到操作接口之中,然后将每个操作封装在实现该操作接口的类中(这里称之为操作类),只需要将该操作类的实例插入到上下文中即可。可以通过将操作类的实例作为参数传递给上下文对象的构造函数,或者通过将操作类的实例作为参数传递给上下文的具体执行方法来完成。

下面展示了如何将上述示例分隔为上下文和操作接口。 StreamProcessor (操作接口)作为参数传递给 StreamProcessorContextprocessStream() 方法。

// 流处理插件接口
public interface StreamProcessor {
    public void process(int input);
}
// 流处理上下文类
public class StreamProcessorContext{
    // 将 StreamProcessor 操作接口实例化并作为参数
    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;
            }
        }
    }
}

现在可以像下面示例一样使用 StreamProcessorContext 类打印出流内容:

FileInputStream inputStream = new FileInputStream("myFile");
// 通过实现 StreamProcessor 接口的匿名子类传递操作实例
new StreamProcessorContext()
.processStream(inputStream, new StreamProcessor(){
    public void process(int input){
        System.out.print((char) input);
    }
});

或者像下面这样读取输入流内容并添加到一个字符序列中:

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();

正如你所看到的,通过插入不同的 StreamProcessor 接口实现来对流做任何操作。一旦 StreamProcessorContext 被完全实现,你将永远不会有关于未关闭流的困扰。

上下文重用非常强大,可以在流处理之外的许多其他环境中使用。一个明显的用例是正确处理数据库连接和事务(open - process - commit()/rollback() - close())。其他用例是 NIO 通道处理和临界区中的线程同步(lock() - access shared resource - unlock())。它也能将API的已检查异常转换为未检查异常。

当你在自己的项目中查找适合上下文重用的代码时,请查找以下操作模式:

  • 常规操作之前(general action before)
  • 特殊操作(special action)
  • 常规操作之后(general action after)

当你找到这样的模式时,前后的常规操作就可能实现上下文重用。

上下文作为模板方法

有时候你会希望在上下文中有多个插件点。如果上下文由许多较小的步骤组成,并且你希望上下文的每个步骤都可以自定义,则可以将上下文实现为模板方法。模板方法是一种 GOF 设计模式。基本上,模板方法将算法或协议分成一系列步骤。一个模板方法通常作为一个单一的基类实现,并为算法或协议中的每一步提供一个方法。要自定义任何步骤,只需创建一个扩展模板方法基类的类,并重写要自定义的步骤的方法。

下面的示例是作为模板方法实现的 JdbcContext。子类可以重写连接的打开和关闭, 以提供自定义行为。必须始终重写 processRecord(ResultSet result) 方法, 因为它是抽象的。此方法提供不属于上下文的操作,在使用 JdbcContext 的不同情况下的操作都不相同。这个例子不是一个完美的 JdbcContext 。它仅用于演示在实现上下文时如何使用模板方法。

public abstract class JdbcContext {
    DataSource dataSource = null;
    // 无参数的构造函数可以用于子类不需要 DataSource 来获取连接
    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)  方法
    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);
            }
        }
    }
}

这是扩展 JdbcContext 以读取用户列表的子类:

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);
    }
}

下面是如何使用 ReadUsers 类:

ReadUsers readUsers = new ReadUsers(dataSource);
readUsers.execute("select * from users", new Object[0]);
List users = readUsers.getUsers();

如果 ReadUsers 类需要从连接池获取连接并在使用后将其释放回该连接池,则可以通过重写 openConnection()closeConnection(Connection connection) 方法来插入该连接。

注意如何通过方法重写插入操作代码。 JdbcContext 的子类重写 processRecord 方法以提供特殊的记录处理。 在StreamContext示例中,操作代码封装在单独的对象中,并作为方法参数提供。实现操作接口 StreamProcessor 的对象作为参数传递给 StreamContext 类的 processStream(...) 方法。

实施上下文时,你可以使用这两种技术。 JdbcContext 类可以将实现操作接口的 ConnectionOpenerConnectionCloser 对象作为参数传递给 execute 方法,或作为构造函数的参数。就我个人而言,我更喜欢使用单独的操作对象和操作接口,原因有两个。首先,它使得操作代码可以更容易单独进行单元测试;其次,它使得操作代码在多个上下文中可重用。当然,操作代码也可以在代码中的多个位置使用,但这只是一个优势。毕竟,在这里我们只是试图重用上下文,而不是重用操作。

结束语

现在你已经看到了两种不同的重用代码的方法。经典的功能重用和不太常见的上下文重用。希望上下文的重用会像功能重用一样普遍。上下文重用是一种非常有用的方法,可以从 API 的底层细节(例如JDBC,IO 或 NIO API等)中抽象出代码。特别是如果 API 包含需要管理的资源(打开和关闭,获得并返回等)。

persistence/ORM API、Mr.Persister 利用上下文重用来实现自动连接和事务生命周期管理。 这样用户将永远不必担心正确打开或关闭连接,或提交或回滚事务。Mr.Persister 提供了用户可以将他们的操作插入的上下文。 这些上下文负责打开,关闭,提交和回滚。

流行的 Spring 框架包含大量的上下文重用。 例如 Springs JDBC 抽象。 Spring 开发人员将其使用上下文重用作为“控制反转”。 这不是 Spring 框架使用的唯一一种控制反转类型。 Spring 的核心特性是依赖注入 bean 工厂或“应用程序上下文”。 依赖注入是另一种控制反转。

Linux公社的RSS地址 : https://www.linuxidc.com/rssFeed.aspx

本文永久更新链接地址: https://www.linuxidc.com/Linux/2019-06/158976.htm

原文  https://www.linuxidc.com/Linux/2019-06/158976.htm
正文到此结束
Loading...