靜態替換

2004 年 10 月 20 日

當我聽我們的開發團隊談論他們的工作時,一個共同的主題是他們不喜歡靜態中的事物。我們通常會看到使用靜態初始化項式的靜態變數中包含共用服務或元件。靜態(在大部分語言中)的一大問題是,您無法使用多型來以一個實作替換另一個實作。這對我們來說是個大問題,因為我們是測試的忠實愛好者,而要進行良好的測試,能夠以 服務 Stub 替換服務非常重要。

以下是這種靜態的範例。

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

因此,我們這裡有一堆在靜態初始化項式中初始化的組態資料,然後有一個靜態方法用於對資料庫執行查詢。

有些變更使用這個方法很簡單。透過變更屬性檔,我們可以輕鬆變更此程式執行的資料庫。但對於測試,我們可能完全不想對資料庫執行程式,一個簡單的 Stub 只會傳回罐頭資料。

為了允許簡單替換,我們需要進行一些重構。第一步是將靜態轉換成單例。

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());。只要 Stub 子類別化 AddressBook,我們現在就可以針對 Stub 進行測試,而不是實際情況。

這並不是故事的結局。特別是使用這段程式碼,我們必須建立實際服務的實例,即使我們從未使用過它,因為唯一的實例是在靜態初始化程式中初始化的。這會強制依賴於服務存取程式碼,而這可能會造成自己的問題。為了處理這個問題,我們需要將任何此類初始化移出靜態初始化程式,並移到一個可替換的獨立初始化類別中。(請參閱 Chris 以了解更多資訊。)但至少這提供了有用的第一步。

這也帶出了一些單例可能儲存的問題。特別是如果您使用單例(或其他形式的 註冊表),請確保它們可以輕鬆替換,並且它們的初始化也可以輕鬆替換。

我剛拿到 Michael Feathers 的新書 有效處理舊有程式碼。他更深入(也更好)地探討了許多這類問題。