JavaでLoan Pattern(をやってみたかった)

Scalaで関数を使ってC#のusing句(リソースの解放忘れを防ぐステキ構文)みたいなことが出来るらしい。

リンク先より引用

type Closable = { def close():Unit }

def using[A <: Closable,B]( resource:A )( f:A => B ) = try {
   f(resource) 
} finally { resource.close }
scala> using( Source.fromFile("/Users/ozaki/testdata.txt") ){ _.getLines.mkString }
res1: String = aaabbbccc

usingにオープンしたリソースと関数を渡すと、そのリソースを引数とする関数を実行した後、必ずクローズしてくれるという具合。
クローズ処理を書き忘れて、世界を股にかける数十億ドル規模の巨大航空会社がストップしたりしなくなる(たぶん。)
Javaでも匿名内部クラスで似たようなことできるかも……やってみた。

まとめ

  • 一応それっぽく安全にはできそう
  • LLのクロージャの代わりに匿名内部クラスを使うと、見た目的にちょっと野暮ったい
    • Genericsを使うとさらに長くなる
  • 複数種類の例外を投げたり途中でreturnしている場合は匿名内部クラスでは難しそう
    • 実用的なレベルで言うとちょっと苦しい
  • こうやって見るとJava 7 or 8で導入される予定のこいつらは互いに無関係では無そう
    • 「Automatic resource management」
    • 「Lambda」(Closure)
    • 「Improved Exception Handling」(Multicatch)

その1. シンプルなバージョン

java.sql.Connectionを使った後close()漏れが無いようにする。
戻り値なしで独自の例外も投げないシンプルな処理を想定

package sample.loan;

import java.sql.Connection;
import java.sql.SQLException;

public class LoanPattern {
    /**
     * Connectionを使った処理を書く為のインタフェース
     * @throws SQLException メソッド実行中にSQLExceptionを投げても良い
     */
    public interface Block {
        void call(Connection connection) throws SQLException;
    }
    
    /** using句の代わりとなるメソッド */
    public static void using(Connection connection, Block block) throws SQLException {
        try {
            block.call(connection);
        } finally {
            connection.close(); // SQLExceptionが上書きされるのが嫌ならtry-catchしてもよい
        }
    }
}

使用例(巨大航空会社の例で)
import staticして使うと良い

import static sample.loan.LoanPattern.using;
...
    private List lookupByCity(...) throws SQLException, RemoteException {
        using(connectionPool.getConnection(), new Block() {
            public void call(Connection conn) throws SQLException {
                Statement stmt = null;
                try {
                    conn = connectionPool.getConnection();
                    stmt = conn.createStatement();
                    // 検索のロジックを実行して、
                    // 結果のリストを返す
                } finally {
                    if (stmt != null) {
                        stmt.close();
                    }
                }
            }
        });
        // この時点でConnectionはclose()されている
	...            
    }

finallyでConnectionをcloseしなくても安心。
finallyでcloseするだけの為にブロック外に変数宣言する必要も無い。

その2. Statementもusingしたい

折角なので念のためにjava.sql.Statementも忘れずにcloseできて欲しいよね。
Connection#close()とStatement#close()は別のメソッドで一つには出来ないので、別々に用意してやる。
但しBlockはGenericにしている。

public class LoanPattern {
    /**
     *  GenericなBlock
     *  @param <R> リソースの型 (ConnectionとかStatementとか)
     * @throws SQLException メソッド実行中にSQLExceptionを投げても良い
     */
    public interface Block<R> {
        void call(R resource) throws SQLException;
    }
    
    /**
     * using句の代わりとなるメソッド。Connectionを使う場合に使用
     * call()終了時に必ずConnectionをcloseする。
     */
    public static void using(Connection connection, Block<Connection> block) throws SQLException {
        try {
            block.call(connection);
        } finally {
            connection.close();
        }
    }
    /** using句の代わりとなるメソッド。Statementを使う場合に使用 */
    public static void using(Statement statement, Block<Statement> block) throws SQLException {
        try {
            block.call(statement);
        } finally {
            statement.close();
        }
    }    
}

使用例

import static sample.loan.LoanPattern.using;
...
    private List lookupByCity(...) throws SQLException, RemoteException {
        using(connectionPool.getConnection(), new Block<Connection>() {
            public void call(Connection conn) throws SQLException {
                using(conn.createStatement(), new Block<Statement>() {
                    public void call(Statement stmt) throws SQLException {
                        // 検索のロジックを実行して、
                        // 結果のリストを返す
                    }
                });
                // この時点でStatementは間違いなくclose()されている
            }
        });
        // この時点でConnectionもStatementも間違いなくclose()されている
	...
    }

オーバーローディングによって、使おうとしているリソースの型に合致する方のusing()メソッドが使われる。
既にインデントが深いし記述が多くて読みにくい。

その3. 戻り値と例外をGeneric化

安全にはなったかもしれないけど、戻り値がないし、ブロックの中からはSQLExceptionしか投げられないのは不便ではある。
例えば、「検索のロジックを実行して、」のブロックでListを生成して戻したいし、その過程でParseExceptionが発生する処理を呼び出している、というような場合を考える。

public class LoanPattern {
    /**
     * GenericなBlock
     * @param <R> リソース
     * @param <T> 指定した例外の型
     * @throws SQLException メソッド実行中にSQLExceptionを投げても良い
     * @throws T メソッド実行中に指定した例外を投げても良い
     */
    public interface Block<R, S, T extends Throwable> {
        S call(R resource) throws T, SQLException;
    }
    
    /**
     * using句の代わりとなるメソッド。Connectionを使う場合に使用。
     * call()終了時に必ずConnectionをcloseする。
     * @param <R> リソースの型
     * @param <S> 戻り値の型
     * @param <T> 例外の型
     * @return S {@link Block#call(R)}の戻り値をそのまま戻す。
     */
    public static <S, T extends Throwable>
            S using(Connection connection, Block<Connection, S, T> block) throws T, SQLException {
        try {
            return block.call(connection);
        } finally {
            connection.close();
        }
    }
    /**
     * using句の代わりとなるメソッド。Statementを使う場合に使用。
     * (略)
     */
    public static <S, T extends Throwable>
            S using(Statement statement, Block<Statement, S, T> block) throws T, SQLException {
        try {
            return block.call(statement);
        } finally {
            statement.close();
        }
    }
}

使用例

    private List lookupByCity(...) throws SQLException, RemoteException {
        try {
            List<Flights> results = 
            using(connectionPool.getConnection(), new Block<Connection, List<Flights>, ParseException>() {
                public List<Flights> call(Connection conn) throws ParseException, SQLException {
                    return 
                    using(conn.createStatement(), new Block<Statement, List<Flights>, ParseException>() {
                        public List<Flights> call(Statement stmt) throws ParseException, SQLException {
                            List<Flights> results = new ArrayList<Flights>();
                            // 検索のロジックを実行して、
                            // (なにかParseExceptionを投げるかもしれない処理)
                            // 結果のリストを返す
                            return results;
                        }
                    });
                }
            });
            return results;
        } catch (IOException e) {
            e.printStackTrace();
            ...
        }
    ....

これはちょっとやりすぎな気がする。しかもこれだけやってもまだあまり実用的ではない。
複数の例外が書けないので、匿名内部クラス内で複数の種類のExceptionを投げたい場合に困る。
大域脱出の手段が例外ぐらいしか無いのとか不便な気がする。


ちなみに戻り値はともかく、引数は増やせないの?という心配は不要。引数を経由しなくても、usingの外側の変数をfinal宣言すれば匿名内部クラスの内部で直接使える。

        // final定義すると...
        final SimpleDateFormat formatter = new SimpleDateFormat(DATE_PATTERN);

        try {
            List<Flights> results = 
            using(connectionPool.getConnection(), new Block<Connection, List<Flights>, ParseException>() {
                public List<Flights> call(Connection conn) throws ParseException, SQLException {
                    return 
                    using(conn.createStatement(), new Block<Statement, List<Flights>, ParseException>() {
                        public List<Flights> call(Statement statement) throws ParseException, SQLException {
                            List<Flights> results = new ArrayList<Flights>();
                            ...
                            // ...匿名内部クラスの内部でも参照できる
                            Date date = formatter.parse(rs.getString(1)); 
                            ...
                            return results;
                        }
                    });
                }
            });
        } catch (ParseException e) {
            ...

蛇足. もうなにかよくわからないもの

ConnectionとStatementだけじゃなくて、任意のリソースの後処理を書きたいとする。
close()可能なリソースの型Closableを作ってくるんでやる。

    /**
     * 後処理を行える汎用のリソース
     * @param <R>リソースの型
     * @param <Tc> リソースクローズ時に起こる例外
     */
    public interface Closable<R, Tc extends Throwable> {
        R getResource();
        void close() throws Tc;
    }
    /**
     * GenericなBlock
     * @param <R> リソース
     * @param <S> 戻り値の型
     * @param <Tu> 指定した例外の型
     * @param <Tc> リソースクローズ時に起こるのと同じ例外
     * @throws Tu メソッド実行中に指定した例外を投げても良い
     * @throws Tc メソッド実行中にリソースクローズ時に起こるのと同じ例外を投げても良い
     */
    public interface Block<R, S, Tu extends Throwable, Tc extends Throwable> {
        S call(R resource) throws Tu, Tc;
    }
    /**
     * 汎用のusing
     */
    private static <R, S, Tu extends Throwable, Tc extends Throwable>
            S using(Closable<R, Tc> closable, Block<R, S, Tu, Tc> block) throws Tu, Tc {
        try {
            return block.call(closable.getResource());
        } finally {
            closable.close();
        }
    }

    /* これ以降は各リソースクラス用のメソッド  */
    /**
     * Connection用のUsing
     */
    public static <S, T extends Throwable> S
            using(final Connection connection, Block<Connection, S, T, SQLException> block) throws T, SQLException {
        // ConnectionをcloseするようのClosableを作って渡す
        return using(new Closable<Connection, SQLException>() {
            public Connection getResource() {
                return connection;
            }
            public void close() throws SQLException {
                connection.close();
            }
        }, block);
    }
    /**
     * InputStream用のUsing。
     */
    public static <S, T extends Throwable> S
            using(final java.io.InputStream stream, Block<InputStream, S, T, IOException> block) throws T, IOException {
        // InputStreamをcloseする用のClosableを作って渡す
        return using(new Closable<InputStream, IOException>() {
            public InputStream getResource() {
                return stream;
            }
            public void close() throws IOException {
                stream.close();
            }
        }, block);
    }
    ...

ちなみにjava.io系のclose()メソッドはjava.io.Closeableというインタフェースに集約されているので、ラッパーが無くてもまとめて書けるかも。


新たにリソースクラスが増えるごとにusingメソッドを定義するのではなくて、毎回呼び出す側でcloseの方法も定義してやってはどうか。
ClosableとBlockが二個あるのは面倒なのでBlock一つにまとめる。
折角なのでリソースの生成もBlock側からメソッドで呼び出すようにする。
call()とかclose()もそれらしい名前に変えてみる。

package sample.loan;

/**
 * @param <R> リソースの型
 * @param <S> ブロックの戻り値の型
 * @param <Tu> リソース生成時の例外の型
 * @param <Tt> ブロック実行時の例外の型
 * @param <Tf> リソースクローズ時の例外の型
 */
public abstract class Block <R, S, Tu extends Throwable, Tt extends Throwable, Tf extends Throwable> {
    /** リソースを生成する */
    protected abstract R using() throws Tu; 
    /** リソースを使用して何か処理をする */
    protected abstract S try_(R resource) throws Tt;
    /** リソースをクローズする */
    protected abstract void finally_(R resource) throws Tf;

    /**
     * ブロックを実行して結果を得る
     */
    public final S evaluate() throws Tu, Tt, Tf {
        R resource = using();
        try {
            return try_(resource);
        } finally {
            finally_(resource);
        }
    }
}

使用例: ファイルの中身を読み込む時に、ファイルのクローズを忘れずにする。

import sample.loan.Block;
...
    public List<String> getContentOfFile(final File file, final String encoding) throws IOException {
        return new Block<InputStream, List<String>, FileNotFoundException, IOException, IOException>() {
            // リソースを生成する
            protected InputStream using() throws FileNotFoundException  {
                return new FileInputStream(file);
            }
            // リソースを使用して処理
            protected List<String> try_(InputStream inputStream) throws IOException  {
                List<String> lines = new ArrayList<String>();
                BufferedReader reader =
                    new BufferedReader(new InputStreamReader(inputStream, encoding));
                String line;
                while ((line = reader.readLine()) != null) {
                    lines.add(line);
                }
                return lines;
            }
            // リソースの後処理
            protected void finally_(InputStream inputStream) throws IOException  {
                inputStream.close();
            }
        }.evaluate();
    }


遠くから見れば、なにか新しい制御構文のようなものに見えなくもない。

import sample.loan.Block;
...
    public ArrayList<String> getContentOfFile(final File file, final String encoding) throws IOException {
        return new Block<InputStream, ArrayList<String>, FileNotFoundException, IOException, IOException>()
        {
                                                                                        protected InputStream
            using                                                                     ()throws FileNotFoundException
            {
                return new FileInputStream(file);
            }
                                                                                        protected ArrayList<String>
            try_(InputStream inputStream)                                               throws IOException
            {
                List<String> lines = new ArrayList<String>();
                BufferedReader reader =
                    new BufferedReader(new InputStreamReader(inputStream, encoding));
                String line;
                while ((line = reader.readLine()) != null) {
                    lines.add(line);
                }
                return lines;
            }
                                                                                        protected void
            finally_(InputStream inputStream)                                          throws IOException
            {
                inputStream.close();
            }
        }.evaluate();
    }

かなり苦しい。
こんなもの書かされるぐらいなら普通に気をつけてfinally書きたい。
というかやっている内容としてはまるきりTemplate Methodパターンではないか。


Javaクロージャ要らねえとか言ってたんだけど、やっぱりあった方がいいかもなあ……