クラスがメソッドの実行に必要なインスタンスを手に入れる方法色々
あるクラスが、メソッドによってある役割を果たすためには、別のインスタンスが必要なことが多い。ここでは、具体的にそのクラスを考え、そのインスタンスのを手に入れる方法を比較していこう。
ここでは、あるクラスをSqlExecutorとしよう。SQL文を受け取って、データベースのConnectionを使って実行するクラスだ。そして、SQL実行結果(ResultSet)をResultHandlerに渡して処理をする。さて、このクラスが責務を果たすためには「SQL文」「データベースConnection」「ハンドラ」の3つのインスタンスが必要だ。このクラスをいくつか書いて、比較してみよう。
どのインスタンスを、どのように受け取るかの違いだ。各インスタンスに対して、「setterで受け取る方法」「コンストラクタで受け取る方法」「メソッド引数で受け取る方法」がある。
A.全てsetterで受け取る方法
import java.sql.Connection; public class SqlExecutor { private Connection con; private String sql; private ResultHandler handler; // 上記setter/getter省略 public void execute() { boolean backup = con.getAutoCommit(); con.setAutoCommit(false); Statement stmt = null; ResultSet rs = null; try { stmt = con.createStatement(); if (stmt.execute(sql)) { rs = stmt.getResultSet(); } handler.handleResultSet(rs); con.commit(); } catch (SQLException e) { con.rollback(); } finally { con.setAutoCommit(backup); JmIOUtil.closeQuietly(rs); JmIOUtil.closeQuietly(stmt); } } }
B.全てコンストラクタで受け取る方法
import java.sql.Connection; public class SqlExecutor { private final Connection con; private final String sql; private final ResultHandler handler; public SqlExecutor(Connection con, String sql, ResultHandler handler) { // フィールド初期化省略 } public void execute() { // 同じなので省略 } }
C.全てメソッド引数で受け取る方法
import java.sql.Connection; public class SqlExecutor { public void execute(Connection con, String sql, ResultHandler handler) { // 同じなので省略 } }
D.conをコンストラクタで、その他をメソッド引数で受け取る方法
import java.sql.Connection; public class SqlExecutor { private final Connection con; public SqlExecutor(Connection con) { // フィールド初期化省略 } public void execute(String sql, ResultHandler handler) { // 同じなので省略 } }
解説
上記のどれも、出来る事には変わりはない。そして上に挙げた以外にもクラスの書き方はたくさんある。3つのインスタンスについて各3通りの受け取り方があるわけだから、合計27種類の書き方がある。…さて、どれを採用すべきなのだろうか?
ということを考えるのが「API設計」作業の一つだ。この答えは、単純に「これを選んでおけば間違いない」という解は存在しない。そのクラス(SqlExecutor)には何が求められているのか、クライアント(このクラスを使うコードや人)の期待によって異なるのだ。API設計は、この「クライアントの期待」を予測して、最適な解を選ぶ作業だ。
前提として、クライアントはSqlExecutorに対して「使いやすい」ことを求めている。これは、どんなクラスに対しても同じことだろう。使いにくいAPIを求める訳がない。初めて使う場合でも使い方が分かりやすく、間違いを犯しにくいAPIを求めているのだ。
では、上記のA〜Dについて、クライアントのコードがどうなるかを書き出してみよう。
Aのクライアント
Connection con = ...; String sql = ...; ResultHandler handler = ...; SqlExecutor sqlExecutor = new SqlExecutor(); sqlExecutor.setConnection(con); sqlExecutor.setSql(sql); sqlExecutor.execute();
クライアントはまずSqlExecutorのインスタンスをnewする。ここで「さて、私はどうしたらいいだろう?」と考えると思う。まぁSQLを実行したいのだから、ConnectionとSQLを渡さなければならないだろう。ということでAPIを見渡すと、各種setterとexecuteがある。ということで、conとsqlをsetして、executeを呼ぶのだ。setResultHandlerはよくわからん。とにかくexecuteを呼んでしまおう。と、その結果、NullPointerExceptionが飛ぶ。あー、使いにくい、という訳だ。人によっては、conさえもsetし忘れるかもしれない。
Bのクライアント
SqlExecutor sqlExecutor = new SqlExecutor(con, sql, handler);
sqlExecutor.execute();
次にBを見てみる。SqlExecutorをnewしようとする。おっと、コンストラクタに引数が必要だ。なるほど、conとsqlとhandlerを求められている。handlerってなんだ? ということで ResultHandler のjavadocなんかを見たりする(ここでは、javadocをコードに明示していないが、書いてあるものとしてください)。ほう、実行結果のハンドラか、と分かってもらえる。さらにnullを渡してはいけない、と書いてある(ことにしてください)。ひとまず何もする必要がないから、「何もしないハンドラ」でも渡しておくか、となる。
さて、使いにくいかもしれないのはこの後だ。クライアントが「複数回SQLを投げたい」と考えたとする。そうなると、新しいSqlExecutorのインスタンスをもう一度newしなければならない。同じconに対する操作なのだが、毎回newしなきゃいかんのか…。少々使いにくいかなぁ…。
Cのクライアント
SqlExecutor sqlExecutor = new SqlExecutor();
sqlExecutor.execute(con, sql, handler);
次にC。newするまではOK。そしてメソッドを眺めるとexecuteメソッドしかない。そして引数に3つのインスタンスを求められる。上記と同じ思考で3つのインスタンスを手配してメソッドに突っ込む…。これだけ? っていうか、これならわざわざSqlExecutorのインスタンス作らずに、staticメソッドを備えたユーティリティにすりゃいいんじゃないのかな。というフシギな感覚をクライアントに与えることになる。
Dのクライアント
SqlExecutor sqlExecutor = new SqlExecutor(con);
sqlExecutor.execute(sql, handler);
さて、D。結論から言えば私はこれが一番バランスが良いと考えている。Aと違ってexecuteまでに「全ての必須要素が要求される」し、Bと違って(同じconに対して使う以上)「同じSqlExecutorインスタンスを使い回すこともできる」し、Cと違ってstaticなユーティリティではなく「インスタンスを作る意義もある」(conを握るから)。
Dは「複数のconに対して同じSQLを投げたい場合はSqlExecutorをnewし直す必要があるじゃん」と思うかもしれないが、まぁ、そういったケースは希であろうと「予測する」。
まぁ、さらにベターなAPIに関して考えると「handlerは要らないケースもあるんじゃない?」とか議論の余地はあるけど、その話はひとまず脇に置いておく。
conは大抵固定で、sqlは違うものを複数回投げたいこともある、という想定で、そしてhandlerというものが「必須なんだ」と認識させることができるAPI。(前述の通り、本当に必須なのかは別議論とします)
こういうAPI設計をしているとき、わたしゃ幸せを感じますw