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(操作接口)作为参数传递给StreamProcessorContext的processStream()方法。

// 流处理插件接口
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类可以将实现操作接口的ConnectionOpener和ConnectionCloser对象作为参数传递给execute方法,或作为构造函数的参数。就我个人而言,我更喜欢使用单独的操作对象和操作接口,原因有两个。首先,它使得操作代码可以更容易单独进行单元测试;其次,它使得操作代码在多个上下文中可重用。当然,操作代码也可以在代码中的多个位置使用,但这只是一个优势。毕竟,在这里我们只是试图重用上下文,而不是重用操作。

结束语

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

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

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

以上所述是小编给大家介绍的Java代码重用之功能与上下文重用,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • Java代码重用之功能与上下文重用

    我几乎不需要讨论为什么重用代码是有利的.代码重用通常使得程序开发更加快速,并使得 BUG 减少.一旦一段代码被封装和重用,那么只需要检查很少的一段代码即可确保程序的正确性.如果在整个应用程序中只需要在一个地方打开和关闭数据库连接,那么确保连接是否正常则容易的多.但我确信这些你已经都知道了. 有两种类型的重用代码,我称它们为重用类型: 功能重用(Action Reuse) 上下文重用(Context Reuse) 第一种类型是功能重用,这是最常见的一种重用类型.这也是大多数开发人员掌握的一种.即重

  • java代码实现截图功能(屏幕截图)

    复制代码 代码如下: import java.awt.Dimension;import java.awt.Rectangle;import java.awt.Robot;import java.awt.Toolkit;import java.awt.image.BufferedImage;import java.io.File; import javax.imageio.ImageIO; /*****************************************************

  • 如何提高java代码的重用性

    提高java代码可重用性有哪些方法措施,以下就讲解了三种关于提高java代码可重用性的措施,一起来了解一下吧~ 一.改写类的实例方法 通过类继承实现代码重用不是精确的代码重用技术,因此它并不是最理想的代码重用机制.继承总是带来一些多余的方法和数据成员,它们总是使得重用类里面某个方法的代码复杂化. 另外,派生类对父类的依赖关系也使得代码进一步复杂化:对父类的改动可能影响子类:修改父类或者子类中的任意一个类时,我们很难记得哪一个方法被子类覆盖.哪一个方法没有被子类覆盖:最后,子类中的覆盖方法是否要调

  • Java实现FTP服务器功能实例代码

    FTP(File Transfer Protocol 文件传输协议)是Internet 上用来传送文件的协议.在Internet上通过FTP 服务器可以进行文件的上传(Upload)或下载(Download).FTP是实时联机服务,在使用它之前必须是具有该服务的一个用户(用户名和口令),工作时客户端必须先登录到作为服务器一方的计算机上,用户登录后可以进行文件搜索和文件传送等有关操作,如改变当前工作目录.列文件目录.设置传输参数及传送文件等.使用FTP可以传送所有类型的文件,如文本文件.二进制可执

  • java实现python session功能代码实例

    这篇文章主要介绍了java实现python session功能代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 怎么在java中实现类似于python的requests模块的session功能呢.java也是可以实现的,用java的okhttp包可以实现. 在pom.xml中加入相关依赖 <!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp --> <

  • java实现选中删除功能的实例代码

    分析: 通过form表单传递数据,删除数据根据id编号删除. 前台 <a class="btn btn-primary" href="javascript:void(0);" rel="external nofollow" id="delSelected">删除选中</a> </div> <form id="form" action="${pageCont

  • Java调用微信支付功能的方法示例代码

    Java 使用微信支付 前言百度搜了一下微信支付,都描述的不太好,于是乎打算自己写一个案例,希望以后拿来直接改造使用. 因为涉及二维码的前端显示,所以有前端的内容 一. 准备工作 所需微信公众号信息配置 APPID:绑定支付的APPID(必须配置) MCHID:商户号(必须配置) KEY:商户支付密钥,参考开户邮件设置(必须配置) APPSECRET:公众帐号secert(仅JSAPI支付的时候需要配置) 我这个案例用的是尚硅谷一位老师提供的,这里不方便提供出来,需要大家自己找,或者公司提供 二

  • java代码实现斗地主发牌功能

    本文实例为大家分享了java实现斗地主发牌功能的具体代码,供大家参考,具体内容如下 实现斗地主发牌功能 共54张牌,地主比其他两名玩家多三张牌. 有一个card牌类和player玩家类,还有一个发牌类用于实现发牌的方法. 为了模拟每个玩家的牌都是随机的,我是这样想的: 1)初始化方法:用于将54张牌存到一个数组里,每张牌都一个唯一的序号. 2) 利用随机数,将每个序号打乱存到一个新数组里. 3)再根据序号取到初始化牌库数组内的牌,存到每个玩家的牌集合内. 附一个在老师指导下写的:斗地主发牌功能,

  • JAVA实现用户抽奖功能(附完整代码)

    需求分析 1)实现三个基本功能:登录.注册.抽奖. 2)登录:用户输入账号密码进行登录,输入账号后会匹配已注册的用户,若输入用户不存在则退出,密码有三次输入机会,登录成功后主界面会显示已登录用户的账号信息. 3)注册:用户首先输入账号名称,系统查询此名称是否存在,如存在则请求用户换一个名称,否则进入密码输入,密码要求6位数字字符串,注册成功后,系统随机分配一个与已有用户不重复的四位数字id编号. 4)抽奖:功能实现前提:需有用户处于登录状态.该前提满足时,系统从已存在用户中随机抽取5位不同的用户

  • Java实现断点下载功能的示例代码

    目录 介绍 效果 前端代码 后端代码 介绍 当下载一个很大的文件时,如果下载到一半暂停,如果继续下载呢?断点下载就是解决这个问题的. 具体原理: 利用indexedDb,将下载的数据存储到用户的本地中,这样用户就算是关电脑那么下次下载还是从上次的位置开始的 先去看看本地缓存中是否存在这个文件的分片数据,如果存在那么就接着上一个分片继续下载(起始位置) 下载前先去后端拿文件的大小,然后计算分多少次下载(n/(1024*1024*10)) (结束位置) 每次下载的数据放入一个Blob中,然后存储到本

随机推荐