日々常々

ふつうのプログラマがあたりまえにしたいこと。

バージョンアップ時に互換性を損ねてしまう変更

あるいは、利用者にバージョンアップをためらわせる変更の手引き。

前置き

うっかり互換性を損なってしまう変更について書いてみます。

知ってたらやらかさないし、なんてことのない事なのだけれど、 知らなかったら何が駄目なのかわかりづらいし、 原因を突き止めるのとかも難しいのかもしれない。と思ったので。

互換性の話なので、ライブラリやフレームワークなどの提供側としての変更になります。

動かない、けど再コンパイルすれば動く

こんなコードがあって、

interface Hoge<T extends java.io.Serializable> {
    T method();
}

これを使っている、こんなコードがある。

class Hogera {

  public static void main(String... args) {
    Hoge hoge = new Hoge<String>() {
      @Override
      public String method() { return "HOGE"; }
    };

    System.out.println(hoge.method());
  }
}

コンパイルして実行すれば、まあ普通に動くよね。

% javac Hoge.java Hogera.java
% java Hogera
HOGE

ここで、元のコードをこう変更しちゃう。

interface Hoge<T> {
    T method();
}

Hoge.java だけ再コンパイルして実行すると、動かない。

% javac Hoge.java
% java Hogera
Exception in thread "main" java.lang.NoSuchMethodError: Hoge.method()Ljava/io/Serializable;
  at Hogera.main(Hogera.java:9)

使う方のコードも再コンパイルするだけで動く。

% javac Hoge.java Hogera.java
% java Hogera
HOGE

説明

Javaは戻り値の型までを含めてメソッドは識別されます。 で、ジェネリクスで境界を使用していると、境界の型で扱われます。何もしていしてないと java.lang.Object ですね。 こういう時に型引数は考慮されません。つまり、 method()LT とかにはならないの。

分かりづらくするためにジェネリクス使ってるけど、単に戻り型を拡げるだけでも十分だったりする。

String method(); // 変更前
Object method(); // 変更後(違うメソッドになる)

昔、似たような事を書いた(しかしタイトル微妙だな……)のだけれど、 正直なところ当時の私の見通しは甘くて、単にビルドツールを使えば解決するって問題ではありません。 今回もjavaファイルを直接扱っているから分かりづらいのだけれど、たとえば Hoge がjarで、 知らない誰かが作っているものだったら、一緒にコンパイルーなんてしませんよね。

こういう変更が入っているかも、と思うとバージョンアップできなくなります。 たまに、依存ライブラリ間でこういうことが起こったりします。 その瞬間のソースを眺めて脳内コンパイルしてると普通に動いちゃうので、知らないと原因特定が難しかったりする困った話。 クラスファイルを読んでたらわかるだろうけど、普通は添付されてるソース読むよね?

動く、けど再コンパイルできない

こんなコードがあって、

class Fuga {

  static void method(String str) {
    System.out.println("FUGA");
  }
}

これを使っている、こんなコードがある。

class Fugera {

  public static void main(String... args) {
    Fuga.method(null);
  }
}

コンパイルして実行すれば、まあ普通に動くよね。

% javac Fuga.java Fugera.java
% java Fugera
FUGA

ここで、元のコードをこう変更しちゃう。

class Fuga {

  static void method(String str) {
    System.out.println("FUGA");
  }

  static void method(Number str) {
    System.out.println("FUGAGA");
  }
}

Fuga.java だけ再コンパイルして実行すると、これは普通に動く。

% javac Fuga.java
% java Fugera
FUGA

使う方のコードも再コンパイル……できない。

% javac Fugera.java
Fugera.java:4: エラー: methodの参照はあいまいです
    Fuga.method(null);
        ^
  Fugaのメソッド method(String)とFugaのメソッド method(Number)の両方が一致します
エラー1個

仕方ないので修正する。

class Fugera {

  public static void main(String... args) {
    Fuga.method((String)null);
  }
}

ダサい。。。 けれど、これでコンパイルできるようになるし、コンパイル後は元通り動作する。うっかりNumberにキャストしたりすると動作が変わるから、「とにかくコンパイルが通るようにすればいい」みたいな対応をすると二次災害になります。 バグ修正時のバグ混入率は通常よりも高いって言いますし、慌てずに落ち着いて対応しましょう。

説明

nullなんて嫌いだ! 最初のと同じ感じだったりします。Javaコンパイル時に呼び出す先のメソッドをコードよりも厳密に決定します。 メソッドオーバーロードしたとしても別のメソッドとして扱うため、コンパイルできないようなオーバーロードを行っても、 コンパイル後のclassが実行される際は問題となりません。

既存のメソッドを修正するときは気をつけられるけれど、 メソッドを追加して今までコンパイルできていたものができなくなるってのは、油断しがちかもしれない。 そしてまだ同じクラスなら話は簡単なのだけれど、たとえばインタフェースに同じ名前のdefaultメソッドが足されたら同じことが起こる。 メソッド足すときも、やっぱり注意しなきゃです。

動作するかと、コンパイルできるかについて書いた。 2軸があるなら、4パターン出るから、あと2パターンも当然あります。 同じように書こう……と思ったのだけれど……

動かないし、再コンパイルもできない

何の互換も残ってないような。変更というより、破壊?

動くし、再コンパイルしても動く

普通に互換性保ててるから問題ないよね?

互換性、互換性、、、

現実で見たことのあるものから二つを挙げてみたけれど、もちろん互換性を損ねる変更なんてのはこれだけではありません。 というか、変更は必ず何かしらの互換性を損ねるでしょう。 先に挙げた通り、具象メソッドの追加も安全ではありません。 可視性を緩めることで損なわれる互換性もあります。

こういう変更をしてしまうと、一番最初に書いたように、利用者はバージョンアップをしたくなくなります。 すなわち、動いているものを変更したくなくなります。 なので提供側ならば全力で避けねばなりません。

だからといって、ありとあらゆる互換性を維持するなんて愚かな目標を立てるべきだと言いたいわけではありません。 それは変更を、つまり成長を阻害することとイコールになります。

そんな役に立たない妄想よりも、 変更がどのような互換性を損ねるかを把握し、 問題があった時にどう対応すれば良いか、その対応にどのくらいかかりそうか とかを 考えといた方がいいんじゃないかな、と思うわけです。

Javadocに何を書こうか

これは Java Advent Calendar 2015 の 16 日目の記事です。

昨日は @yukung さんの Java で引数の null チェックで迷った話 でした。明日は @mdstoy さんです。

ちなみに過去のJavaアドベントカレンダーで書いたもの。

……2013年は書いてないのか。そうか。

前置き

3年前になりますが、コメントについては書いたことがあります。

たまにスライド見返したりするのだけれど、今でもそれほど主張は変わらない感じです。

で、先に挙げた記事はインラインコメントがメインとなっていますので、 今回はドキュメントコメントについて書いてみることにします。

Javadocと言えば

Javadocと言えばHTMLのあいつ。 JavaDocとか言ったり、javadocというツールのことだったり、 Javadocが何を指してるのかよく分からなくなることもしばしばあるけれど、 とりあえずそのことについては考えずにここではJavadocの表記で統一します。

Java SEのAPI仕様は見たことあるはず。

HTMLで出力したものをWebブラウザで見るので、 コメントの本文はHTMLタグでわかりやすくマークアップしなければならない。

また、Javadocタグを使えることろは積極的に使う。 そうすれば、ドキュメント間のリンクとかもやってくれる。 タグは頑張って覚えよう。そんなに種類もないし。

読み方を考えてみる

Javadocコメントを読む方法は以下の3つに大別できる。

一つは先にも挙げたように、インターネットを介して閲覧する場合。 Java SEやOSSライブラリのドキュメントを読むときはこの形が多いだろう。 また、ドキュメントの記述量が多い場合、IDEで読むのはつらいのでこの読み方をする。

次に、IDEで使用するクラスやメソッドのドキュメントを参照する場合。 この用途では、メソッドのコメントを読むことが主になる。 HTMLで読むものと対象は被るが、コーディング中ならばこちらの方法で読むと思われる。 利用するライブラリのソースが必要だけれど、たいてい xxx-sources.jar は手に入るから心配いらない。

最後に、ソースコードを直接見る場合。 自分たちで開発しているコードのドキュメントコメントはこの形で読むことが多い。 というか、わざわざHTMLで見るなんてしない感じ。 APIとして利用を想定されていないクラスやメソッドのコメントを読む場合もこれになる。 この見方だと、ドキュメントコメント以外のコメントやコードも目に入ってきます

書くことを考えてみる

では何を書けばいいかだけれど、昔はこんなこと書いてた。

このコメントに書かれていて嬉しいことは「何が達成できるか」です。どのように使うと、どうなるかをさくっと把握できるのがベストです。 このコメントを読む人がコードを読むことはそれほど考えなくていいです。

これは一番目、二番目の読み方をする時に言えることで、 三番目の読み方をする場合はコードと並べて読むこともあり、書く内容も変わる。 書く内容は非ドキュメントコメントに近いと思われ、実装理由やコードの要約が適切かもしれない。

読み方から書く内容は決まるかなと思ったのだけれど、読み方分類では書く内容を考えづらい。 なので、読み方よりも読者視点で書くことを考えてみることにする。

読者を考えてみる

コメントの想定読者は利用者と保守担当者くらいが考えられる。 これらの想定読者によって、書く内容も変わってくるはず。

想定読者が利用者、つまり公開APIのドキュメントコメントは、 それが何ができるもので、どのように使い、何に気をつければ良いかを記述する。 サンプルコードがあると良いことも多いのだけれど、 コメントはコンパイルできないし、基本的にリファクタリングに追随して修正されたりもしない。 ゆえに嘘になりやすく、誤ったサンプルコードを提示してしまう危険と隣り合わせになる。 それでもあって助かることも多いので、メインとなる公開APIには書いていきたいと思う。

想定読者が保守担当者、つまり公開APIを除く全てのドキュメントコメントは、 それが何ができるものかは同じだけれど、設計判断や拡張時に気をつけるべきことを記述する。 関連が強いクラスへのリンクをしっかり書いておくと助かることも多い。 privateのドキュメントコメントを書く場合、ほぼ間違いなく保守担当者向けだろう。

ドキュメントコメントにHTMLを使うべきか

素直に「JavadocコメントはHTMLでマークアップするもの」とやってしまうと、 主となる読者が「そのコードを保守する人」であるならば、HTMLは邪魔になる可能性が高い。 人間の目はHTMLを解釈しないので、プレーンなテキストの方が読みやすい。 また、コードを修正する時もHTMLタグを書くのは面倒だったりする。

想定読者が保守担当者のドキュメントコメントにHTMLを使用すると、苦労して書いて、苦労して読んで、苦労して修正することになる。 誰も嬉しくない。 そんなことがあるので、「HTMLを使うべきか」と問われると「状況によるし、使わなくてもいいんじゃないかな」になる。

とはいえHTML的に不正な状態だと文句言われるし、多少配慮はしたほうがいいかも。

あらためて、何を書こうか

Javadocに何を書こうか。

どうせ書くなら役にたつものがいいよね。 役に立てる人によって、書いてて欲しいことが変わるよなーとか、 たぶんプロダクトの性質によっても変わってくる。

でも「何を書くか」なんてところこそ、しっかり考えるべきなんだと思う。 考えて出した答えなら大丈夫なはず。 「書かなきゃいけないから書いた」だと、微妙なコメントになってること請け合い。

……色々書いたけど、結論が締まらない。まぁいいか。

ここまで書いてきたこととかは関係なく書くべきものもある。バージョンとかね。 この辺りはJavadocタグで書けるので、一通り眺めてどれを使うか確認しておくのが良いです。

JavaDoc.Next

あ、Javadocといえば JavaDoc.Next とかいうのもありますね。 詳細はよく知らないけれど、JDK9に入るみたいです。

JavaアドベントカレンダーなのにJavaっぽくなくて焦ったから、 JEPとか出して誤魔化しているわけではない。

ハンズオンイベントに潜む悪魔(黒猫)

11/15に「Javaでwebアプリケーション入門」というイベントのお手伝いをいたしました。 #javajok で関西Java女子部さんのイベントです。

私は参加者と一緒にライブコーディングをするという大役を仰せつかり、緊張のあまり前日は24時間以上爆睡いたしまして。 おかげでGitHubの草を生やそう運動が54日で止まりました。無念。

会場は @bufferings さんに楽天株式会社大阪支社のカフェテリアを提供いただきました。最高ですね。広いし綺麗だし。

同会場にて11月21日(土)に Rakuten Technology Conference 2015 のサテライトが行われます。大阪にいながら世界のセッションを聞けますよ!(英語的な意味で)

当日のコトが起こるまで

家を出てすぐに黒猫に横切られました。嫌な予感がぷんぷんしました。

まず、環境構築でドハマりしました。 ハンズオンイベントでの問題となる筆頭は環境構築です。 ただ、Javaの場合はJDKIDEの準備さえしていてもらえれば、GradleWrapperが他は良いようにしてくれます。 してくれる。そう思っていた時期もありました。参加者が一斉にGradleWrapperを使えば……ダウンロード、すっごいですね。 全然終わりませんでした。

セッション順番を組み替えたり、Gradleを使わずにjarを添付する形にしたり、運用対処でなんとか凌いでハンズオン開始。 準備はできているので大丈夫だと思っていました。各ステップを細かくコミットしたブランチも作って「何かあってもこれを見てもらえばいける」くらいの気分。この準備は前日やる予定だったのですが、残念ながら私の土曜日は夢に飲み込まれたので、仕方なく会場についてから必死にやってました。でも間に合った、いけるつもりでした。

しかし、現実はそうは甘くありません。 本番はAPIサーバーをherokuに立てました。素振りはlocalhostでやりました。検証環境と本番環境が異なると問題が検出できないものです。 ですが素振りの他に、ほぼ同じように作成したサンプルは動作していたので、大丈夫だと思っていました。油断していたのです。

ただでさえ初心者を対象にしたハンズオン。 いくらサポート陣を揃えていても、予定通り進まないのはある程度仕方ありません。 この辺りは織り込み済みで、最悪最後まで行かなくても仕方ないかなーとも思っていました。

起こったコト

ハンズオンの最後の最後。ブラウザで入力したメッセージをつぶやく、よくあるフォーム入力を受け付けるところです。 そこに、以下のようなコードがありました。

Response res = new RestTemplete().postForObject(
        "http://xxxx.heroku.com/yyy",
        param,
        Response.class);

この子がエラーにならずに正常終了しているのに、APIサーバーにはPOSTできませんでした。ちなみにGETの方は普通に動いていたので、タチが悪い。

原因は xxxx.heroku.com/yyy が xxxx.herokuapp.com/yyy にリダイレクトされる動作になっている点。getForXxx はリダイレクトを平然と処理して、リダイレクト先の結果を返してきます。一方 postForXxx はリダイレクトは処理せず(POSTなのでリダイレクトしたらダメだよね)、そのまま301 Moved Permanentlyを空のbodyで受け取って終了しちゃっていました。 この結果になっていることが検出できずに処理は正常終了。最終的には「エラーにならないのに動かない」という不思議な事態となりました。 プロジェクターでスクリーンに画面を映しながら、頭抱えることに……。

結果的には終わる前に解決できて動いたので、それはそれで良いのですが。

言い訳タイム

herokuとかRestTempleteとか普段使ってないのがね……。

postForEntityで受けて、ResponseEntitystatusCodeを見れば判断つくのですが、postForObjectだと直接オブジェクトが生成されちゃうので、見るタイミングがありません。一応RestTemplateに拡張したErrorHandlerを差し込んで「201 Created 以外ならエラー」とかもできますが、どちらにせよ「初心者向けハンズオン」にあまり複雑なことは持ち込みたくはないので、知っていても入れなかったでしょう。(Springのハンズオンでは無いのです)

素振りの時点でherokuを使っておけば、herokuとherokuappの違いに気付けたかもしれません。アドレスを打つときに手打ちせずにコピペしていれば間違えませんでした。URIを手打ちせず、サンプルと同じく設定ファイルから @Value で取っておけば問題は起きなかったです。どれも実際は起こらなかった可能性ですが、助かる可能性もあるにはありました。

しかし、気付くタイミングや間違えないタイミングは全てスルーして問題は起きるものです。油断大敵……そこまでは結構うまくいってた(つもりな)だけに、残念感が酷かったです。

総括

とても楽しかった。今度から黒猫に横切られたら帰って寝る。