静态替换

2004 年 10 月 20 日

当我倾听我们的开发团队谈论他们的工作时,一个共同的主题是他们不喜欢静态的东西。通常我们会看到公共服务或组件存储在带有静态初始化器的静态变量中。静态变量(在大多数语言中)的一个主要问题是,您无法使用多态性来用另一个实现替换一个实现。这让我们很头疼,因为我们非常喜欢测试——而要进行良好的测试,重要的是能够用 服务存根 替换服务。

以下是一个这种静态的示例。

public class AddressBook {
  private static String connectionString, username, password;

  static {
    Properties props = getProperties();
    connectionString =(String) props.get("db.connectionString");
    password = (String) props.get("db.password");
    username = (String) props.get("db.username");
  }

  public static Person findByLastName(String s) {
    String query = 
      "SELECT lastname, firstname FROM PEOPLE where lastname = ?";
    Connection conn = null;
    PreparedStatement st = null;
    ResultSet rs = null;
    try {
      conn = DriverManager.getConnection(connectionString, 
                                         username, 
                                         password);
      st = conn.prepareStatement(query);
      st.setString(1, s);
      rs = st.executeQuery();
      rs.next();
      Person result = new Person (rs.getString(2), rs.getString(1));
      return result;
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      cleanUp(conn, st, rs);
    }
    }

因此,我们这里有一堆在静态初始化器中初始化的配置内容,然后是一个针对数据库运行查询的静态方法。

一些更改很容易实现。通过更改属性文件,我们可以轻松地更改此程序运行的数据库。但是,为了进行测试,我们可能根本不想针对数据库运行它——一个简单的存根只会返回预先准备好的数据。

为了允许简单的替换,我们需要进行一些重构。第一步是将静态变量转换为单例。

public class AddressBook {

    private static AddressBook soleInstance = new AddressBook();

    private String connectionString, username, password;

    public AddressBook() {
        Properties props = getProperties();
        connectionString =(String) props.get("db.connectionString");
        password = (String) props.get("db.password");
        username = (String) props.get("db.username");
    }

    public static Person findByLastName(String s) {
        return  soleInstance.findByLastNameImpl(s);
    }

    public Person findByLastNameImpl(String s) {
        String query = "SELECT lastname, firstname FROM PEOPLE where lastname = ?";
        Connection conn = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        try {
            conn = DriverManager.getConnection(connectionString, username, password);
            st = conn.prepareStatement(query);
            st.setString(1, s);
            rs = st.executeQuery();
            rs.next();
            Person result = new Person (rs.getString(2), rs.getString(1));
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            cleanUp(conn, st, rs);
        }
    }

这是一个非常简单的重构。

  • 我们将旧类上的所有静态数据转换为实例数据。
  • 我们将静态初始化代码移动到构造函数中。
  • 我们将所有公共方法的代码体移动到实例上,并将静态方法保留为简单的委托。

我在目录中没有这个重构——也许我应该称之为“用单例替换静态”。就目前而言,这并没有改变任何东西,但它是支持替换的一步。下一步是引入一个方法来加载唯一的实例。

    public static void loadInstance(AddressBook arg) {
        soleInstance = arg;
    }

现在,这为我们准备了进行测试(或其他)目的的替换。现在,在测试用例中,我们可以在测试 setUp 方法中添加一个合适的调用:AddressBook.loadInstance(new StubAddressBook());。只要存根是 AddressBook 的子类,我们现在就可以针对存根进行测试,而不是针对真实的东西。

这还不是故事的结尾。特别是对于这段代码,我们必须创建实际服务的实例,即使我们从未使用过它——因为唯一的实例是在静态初始化器中初始化的。这迫使我们依赖服务访问代码,这会导致它自己的问题。为了解决这个问题,我们需要将任何此类初始化从静态初始化器中移出,并将其移入一个单独的初始化类,该类本身也是可替换的。(有关更多信息,请参阅 Chris。)但至少这提供了一个有用的第一步。

这也突出了单例可能会积累的一些问题。特别是如果您使用单例(或其他形式的 注册表),请确保它们可以轻松地替换,并且它们的初始化也可以轻松地替换。

我刚收到了一本迈克尔·费瑟斯的新书 有效地使用遗留代码。他更详细地(并且更好)地讨论了这些问题。