静的置換

2004年10月20日

開発チームが自分たちの仕事について話しているのを聞いていると、静的に保持されているものが嫌いだという共通のテーマがあります。通常、共通のサービスやコンポーネントが、静的イニシャライザを持つ静的変数に保持されているのを見かけます。静的変数(ほとんどの言語で)の大きな問題の1つは、ポリモーフィズムを使用してある実装を別の実装に置き換えることができないことです。これは、私たちがテストの大ファンであるため、私たちを大いに悩ませます。そして、適切にテストするためには、サービスをサービスタブに置き換えることができることが重要です。

これは、この種の静的変数の例です。

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

これは非常に簡単なリファクタリングです。

  • 古いクラスのすべての静的データを取得し、インスタンスデータに変換します。
  • 静的初期化コードを移動し、コンストラクタに移動します。
  • すべてのパブリックメソッドを取得し、その本体をインスタンスに移動して、静的メソッドを単純なデリゲータとして残します。

私はこのリファクタリングをカタログに持っていません。おそらく、静的変数をシングルトンに置き換える(Replace Statics With Singleton)と呼ぶべきでしょう。現状ではこれは何も変更しませんが、置換をサポートするためのステップです。次のステップは、唯一のインスタンスをロードするメソッドを導入することです。

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

これにより、テスト(またはその他の)目的で置換を実行する準備ができました。テストケースでは、テストのsetUpメソッドに適切な呼び出しを追加できます:AddressBook.loadInstance(new StubAddressBook());。スタブがAddressBookをサブクラス化している限り、実際のものの代わりにスタブに対してテストを実行できます。

これは物語の終わりではありません。特にこのコードでは、たとえ使用しなくても、実際のサービスのインスタンスを作成する必要があります。これは、唯一のインスタンスが静的イニシャライザで初期化されるためです。これにより、サービスアクセスコードに依存関係が強制され、それ自体が苦痛を引き起こす可能性があります。これに対処するには、静的イニシャライザからの初期化をすべて、それ自体が置換可能な別のイニシャライザクラスに移動する必要があります。(詳細についてはChrisを参照してください。)しかし、少なくともこれは有用な第一歩を提供します。

これにより、シングルトンが抱え込む可能性のある問題もいくつか明らかになります。特に、シングルトン(またはその他の形式のレジストリ)を使用する場合は、それらを簡単に置換でき、初期化も簡単に置き換えられるようにしてください。

Michael Feathersの新しい本、Working Effectively With Legacy Codeのコピーを入手しました。彼は、この種の問題の多くについて(より適切に)述べています。