著作一覧 |
仕事環境でJDK1.8を解禁させたので、いろいろ試しているうちにとんでもないことに気付いた。簡単にインターフェイスが爆発してしまうのだ(あまりうまい表現ではないなぁ。ビッグバンが起きるという雰囲気を出したいのだが)。
ようするに、これまで糞面倒だったコールバックが異様に簡単に書けるようになったので、呼び出し側が簡単に利用できる以上は呼ばれる側も情け容赦なくコールバック関数を取るAPIを作るわけだが、異様に簡単なのはラムダ式を記述できるからだ。ということは、関数型インターフェイスをばかすか定義することになる。
そんなものは1メソッドあたりで定義すれば良いし、呼び出し側はどうせ型宣言とかしないのだから、内部インターフェイスで良いだろうと考える。すると1メソッド平均1.5インターフェイス、1クラスあたり5publicメソッドとすると、1クラスあたり8インターフェイスくらいが定義されてしまう。それが累積するのですごいことになる(クラスファイルは)。
たとえばHttpUrlConnectionをラップした便利クラスを作るとする。ここでの便利というのはドメイン特化と言う意味だ(でも下のサンプルは汎用だけど)。
public class EasyHttp implements Closeable { HttpURLConnection connection; public EasyHttp(String uri) throws Exception { this(uri, null, null); } public EasyHttp(String uri, String user, String pwd) throws Exception { URL url = new URL(uri); connection = (HttpURLConnection)url.openConnection(); if (user != null) { connection.setRequestProperty("Authorization", "Basic " + Base64.getEncoder().encodeToString((user.trim() + ":" + pwd.trim()).getBytes())); } connection.setRequestProperty("Accept-Encoding", "gzip"); } public interface SetupCallback { void setup(HttpURLConnection c) throws Exception; } public void setup(SetupCallback cb) throws Exception { cb.setup(connection); } public interface WriteOperation { void write(OutputStream os) throws IOException; } public interface ReadOperation { void read(InputStream is) throws IOException; } public interface ReadStringOperation { void read(String response) throws Exception; } public interface ErrorOperation { void error(int code, InputStream is) throws Exception; } public int get(ReadOperation ro, ErrorOperation eo) throws Exception { return start(null, ro, null, eo); } public int getString(ReadStringOperation ro, ErrorOperation eo) throws Exception { return start(null, null, ro, eo); } public int post(WriteOperation wo, ReadOperation ro, ErrorOperation eo) throws Exception { connection.setDoOutput(true); return start(wo, ro, null, eo); } // この名前は悪い。StringをPOSTするみたいだ。getStringに合わせてレスポンスをStringで取るという意味だがそうは読めない public int postString(WriteOperation wo, ReadStringOperation ro, ErrorOperation eo) throws Exception { connection.setDoOutput(true); return start(wo, null, ro, eo); } int start(WriteOperation wo, ReadOperation ro, ReadStringOperation rso, ErrorOperation eo) throws Exception { connection.connect(); if (wo != null && connection.getDoOutput()) { try (OutputStream os = connection.getOutputStream()) { wo.write(os); os.flush(); } } String encoding = connection.getHeaderField("Content-Encoding"); boolean gzipped = encoding != null && encoding.toUpperCase().equals("GZIP"); int status = connection.getResponseCode(); if (status == HttpURLConnection.HTTP_OK) { try (InputStream is = (gzipped) ? new GZIPInputStream(connection.getInputStream()) : connection.getInputStream()) { if (ro != null) { ro.read(is); } else { ByteArrayOutputStream bao = new ByteArrayOutputStream(); byte[] buff = new byte[8000]; for (;;) { int len = is.read(buff); if (len < 0) { break; } else if (len == 0) { continue; } bao.write(buff, 0, len); } if (rso != null) { rso.read(bao.toString("UTF-8")); } bao.close(); } } } else if (eo != null) { try (InputStream is = (gzipped) ? new GZIPInputStream(connection.getErrorStream()) : connection.getErrorStream()) { eo.error(status, is); } } return status; } @Override public void close() throws IOException { if (connection != null) { connection.disconnect(); } } }
JDK1.8の型推論がいまいちなのは、上の例だとget, getStringなどとユーザーAPIのメソッド名をオーバーロードではなく別物にせざるを得ない点だ。つまりもし同じ名前にすると、仮にラムダ式の内側で型が明らかに異なっても「参照はあいまいです」というエラーになる点だ。で、しょうがないのでメソッドはオーバーロードせずに名前を変えざるを得ない。
で、とにかくまともになったのは、上の細かなインターフェイス名などをいちいち呼び出し側は書く必要が無い点だ。
// なんとあの見苦しいimport java.io.*をほとんど書く必要がない。 try (EasyHttp eh = new EasyHttp("http://www.yahoo.co.jp")) { eh.getString(s -> System.out.println(s), (code, es) -> System.out.println("error:" + code)); } catch (Exception e) { e.printStackTrace(); }
あるいは
try (EasyHttp eh = new EasyHttp("http://example.com/postdata.aspx")) { eh.setup(c -> c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")); eh.postString(os -> os.write("a=b&c=d".getBytes()), s -> System.out.println(s), (code, e) -> System.out.println("error:" + code)); } catch (Exception e) { e.printStackTrace(); }
呼び出し側は、これがJavaかと驚くほどシンプルに書けるようになって、実に良い。しかしclassesディレクトリを見るとぞっとするほどclassファイルができている。
でちょっと思ったのは、少なくとも楽に関数を引数にできると、テンプレートメソッドパターンはほとんど必要なくなるので(インスタンス変数を持つ側の問題があるからすべてではない)、それほど継承(一番のモチベーションはテンプレートメソッドパターンの適用なので)を使う必要がなくなる。それでJavaScriptはクラスベースではないのに、元々のJavaよりも使いやすいのかな? ということだったりする(今となってはJavaのほうが関数引数は書きやすいわけだが)。
ジェズイットを見習え |