Javadocを書かない
前回はJavadocを書く - しげるメモというタイトルで話を進めましたが、今回は逆にJavadocを減らすプラクティスについてメモがてら。
私は別にJavadocを書くのが好きなわけではなく、単純に書いたほうがめんどくさくないと思うのでそうしてます。ただ、Javadocを書くのもかなりめんどくさいとは自分自身で感じているので、そのめんどくささをできるだけ減らす道を現在も模索中です。
やり方としては単純で、次のうちどちらかです。
かなりの部分がEffective Java (Java Series)に紹介されているプラクティスとかぶりますが、ここではあくまで"めんどくさくないJavadoc"という視点でいきます。
Javadocをそもそも書かない
If an API is to be usable, it must be documented.
上のルールと同居させるには、こんな感じでやります。
インターフェースにJavadocを書いて、実装には書かない
私はインターフェースメソッドだけにJavadocを書いて、その実装には基本的にJavadocを書いていません。javadocツールはメソッドにドキュメンテーションコメントがないと、オーバーライド先のメソッドやインターフェースメソッドから勝手に引っ張ってきてくれます。
このやり方は、Javadocを書くめんどくささを減らすだけじゃなくて副作用(?)でいいことがあります。
- インターフェースとドキュメントだけ別に書ける
- インターフェースのJavadocで決めたことだけを実装するクラスを作れる
- 早い段階でMockを使ってテストできる
- いざとなったら実装を切り替えるのが楽
- java.lang.reflect.Proxy はインターフェースしか使えない
Effective Java の"Item 52: Refer to objects by their interfaces"でもいろいろとインターフェースを使う利点が書いてあります。
APIを公開しない
「ユーザが利用可能なすべてのAPIにJavadocを書く」のならば、公開しなければユーザは利用できないのでJavadocを書かなくてもよくなります。
- 何をするのか明確でない (= whatが不明瞭)
- 利用者を想定できてない (= whyが不明瞭)
- 利用者との契約が多すぎる (→ 設計が不十分?)
のうちどれかだと思っているので、公開しても使われない上に保守もしなきゃならない場合があります。
この副作用(?)は、Effective Java の"Item 13: Minimize the accessibility of classes and members"を同時に満たせることですかね。
Javadocに書くことを減らす
公開APIが存在しないプログラムというのはほとんどないと思いますので、どこかで結局Javadocを書くことになると思います。
私がJavadocに書くべきだと思う項目は、前回のチェックリストを逆算するとこの辺です。
- このメソッドが何をするか
- このメソッドは利用者に何を与えるか (何を返すか、何を変えるか)
- このメソッドを使うにはなにをすればいいのか (事前条件)
- このメソッドはどんな影響を及ぼすのか (事後条件、副作用)
- このメソッドの処理が失敗したとき何が起こるか (異常系)
これらをできる限り削減することでJavadocの記述量を減らしていきますが、書かなきゃならないということは基本的に変わりません。それならば逆にJavadocに書くことが少ないような設計に持っていくことで、Javadoc自体のめんどくささを減らします。
Javadocに必要な情報が書いてあって、さらに短くまとまっているということはメソッドの設計自体が簡潔であるということになり、実装やテストも(ついでに)減るかもしれません。
これらをめんどくさくないようにする方法は多岐に渡るため、やや各論になってしまいがち。
余計なことを書かない
当然ながら、余計なことを書かなければJavadocは短くなります。
public interface Hoge { /** * このオブジェクトに格納されたほげほげを返します。 * 返されるほげほげは{@link java.lang.reflect.Proxy}を使用した * プロクシオブジェクトです。 * このメソッドはGoogle Collections Libraryを使用して * ほげほげを検索しているため、Java Collection Frameworkよりも高速です。 * @return このオブジェクトに格納されたほげほげ */ HogeHoge getHogeHoge();
…恣意的に書いているため、ありえねーと思ってもスルーしてください。ただ、このように書くと「Javadocが長くなる」以上の弊害があります。
Javadocに書いてあることは「実装」ではなく「仕様」になってしまうので、余計なことを書くと実装の幅が限られてチューニングができなくなったり、保守の際にアクロバティックな実装を強要されたりします。
要は、利用者がこのメソッドを使うために必要なことだけがすべてJavadocに書いてあればよく、バランス感覚が重要かと。
IDEにがんばってもらう
Javadocの量を減らさなくても、Javadocを書く量はIDEを使って多少削減できます。
Eclipseを使うと、基本的に@param, @return, @throw (チェック例外のみ)は自動で生成してくれますし、自動補完で@linkを簡単に書いたり、テンプレートを登録したりもできます。私がUMLのクラス図を描きたくないと思うのは、Eclipseを使うとJavadocの補完系が便利すぎてもう引き返せないためです。
ちなみに、使っているテンプレートの一部。
- @return_or_null
- 、存在しない場合は{@code null}
- @nullpointer
- @throws NullPointerException 引数${cursor}が{@code null}の場合
- @illegalargument
- @throws IllegalArgumentException 引数が${cursor}場合
Javadoc系のテンプレートは"@"から始めると、日本語でもそこが自動的に単語区切りとなってイライラしません。
継承させない
Effective Javaには "Item 17: Design and document for inheritance or else hrohibit it" という項目があるくらい、継承は厄介な問題です。ここに "... document for inheritance ..." とあるので、逆に継承を禁止すればそもそもそんなドキュメントは要りません。
継承を禁止する*1には、下の3パターンが有効だと思います。
- クラスをfinalで宣言する
- プライベートコンストラクタのみを提供する
- 運用final
最後のは私の造語で、クラスやインターフェースのJavadocに次の魔法の一言を書くだけです。
アプリケーションでは、このクラスをサブクラス化しないでください。
ちなみにこれはすごい勢いでバッドノウハウですが、Eclipseではよく見かけます (@see "This interface is not intended to be implemented by clients." - Google 検索)。
オブジェクトを変更させない
オブジェクトや環境を変更させるメソッドがなければ、Javadocから副作用やオブジェクトの状態に関する記述を全部削れます。
Effective Javaの "Item 15: Minimize mutability" に詳しくありますが、変更不可能なオブジェクトはいろいろなところで利点があります。
- 常にスレッドセーフ
- 安心して共有できる
ただし、オブジェクトを変更不可能にするには、それなりにいろいろとやらなきゃならないことがあります (これもEffective Javaに詳しくあります)。
- コンストラクタ引数に変更可能なオブジェクトが渡された場合、すべてディープコピーをとって保持する
- @see "Item 39: Make defencive copies when needed"
- メソッドが変更可能なオブジェクトを返す場合、すべてディープコピーを返す
- Collections.unmodifiable*(...) などで、変更不可能なビューを返してもよい
「オブジェクトの生成が無駄だ」と思う方は、一度パフォーマンス計測してみるといいと思います。下手にスレッドセーフのための同期を組むより速くなることが多いですし、スレッドのメモリモデルや最近のGCの仕組みを考えると、ライフサイクルの短い小さなオブジェクトは生成も回収もけっこう速いはずです。
ジェネリクスを使う
過去に何回かジェネリクスについて議論しましたが、ジェネリクスはJavadocのめんどくささを減らすためにも有効です。
List<String> getHogeList(); List getFooList();
前者の"getHogeList"は明らかに文字列のリストを返すことがわかりますが、後者の"getFooList"はJavadocにリストの要素の型まで明記してくれないとデバッガでブレークポイントの対象になります。
前提条件を厳しくする
前提条件を厳しくするとJavadocは逆に短くなるという主張です。次の(極端な)例を見てください。
/** * 二つの文字列を整数とみなし、その和を返す。 */ long add(String a, String b);
もちろんこれではJavadocとしては不十分で、次のようなことを考えなければなりません。
- 整数のフォーマットは?
- 符号がついててもOK?
- 途中にスペースが入っていたら?
- 漢字は?
- 16進数とかは?
- 数値の範囲は?
- 数値を表現しない場合は?
- nullの場合は?
こういう場合に「空文字やnullのときは0として扱う」とか「a~fが含まれていたら16進数として扱う」とか、さまざまな前提条件の緩和方法が思いつき、それを全部Javadocに長々と書くのも方法のひとつです。が、私は単純に下でいいと思います。
/** * 二つの整数の和を返す。 * ... */ long add(int a, int b);
ちなみにこの例は、"Item 50: Avoid strings where other types are more appropriate" にも関連してます。
このやり方がAPIのユーザビリティを考慮した場合に必ず適切であるとは思いませんが、基本的には前提条件を厳しくして、少しでも外れたら無慈悲に例外をスローすればいいと思います。
たとえば、
/** * 引数に指定された3文字の文字列を、逆順に並び替えて返す。 * 引数が3文字に満たない場合は先頭にスペースを加えて * 3文字に調整した後に逆順に並び替えた文字列を返し、 * 引数が3文字を超える場合は4文字目以降を除去して * 3文字に調整した後に逆順に並び替えた文字列を返す。 * ただし、引数に{@code null}が指定された場合はそのまま{@code null}を返す。 ...
というメソッドはちょっと慈悲深すぎで
/** * 引数に指定された3文字の文字列を、逆順に並び替えて返す。 * @param string 3文字の文字列 * @return 対象の文字列を逆順に並び替えた文字列 * @throws IllegalArgumentException * 引数に指定された値が{@code null}または3文字でない場合 */
くらいの拒絶感でやると異常系の記述が短くなってJavadocを書く気にもなります。例にもあるように、前提条件をゆるくしてさまざまなケースに対応しようとすると、本来なら異常である条件をカテゴリ分けして、それぞれをどのように対処するのかをメソッドの仕様として明記しなければなりません。
前提条件を厳しくすることで、ほかのバグを浮き彫りにできるという利点もあります。あまり前提条件をゆるくして異常値を正常地かのように扱ってしまうと、ほかのバグで発生した異常値が見過ごされて正常に動いているかのように錯覚させてしまう場合が多々あります。
ちなみに私の開発環境(Eclipse)では、メソッドのコメントテンプレートが次のようになってます。
/** * ${tags} * @throws NullPointerException 引数に{@code null}が指定された場合 */
ついでに、エディタテンプレートに次のようなものがあります。
if (${cursor}${} == null) { throw new NullPointerException("${}"); //$$NON-NLS-1$$ }
意外と便利です。
フェイルファスト
Effective Javaの "Item 64: Strive for failure atomicity" にもあるように、メソッドの処理中にエラーが発生したらオブジェクトの状態をメソッド呼び出し前にロールバックするのがオススメです。
以下のメソッドで、map.get("a")とmap.get("b")が参照可能だと仮定したとき、下のput2はどんな記述が必要でしょうか。
private Map<String, String> map; ... public void put2(Object a, Object b) { map.put("a", a.toString()); map.put("b", b.toString()); }
実装を見ると、aやbがnullだとエラーになるのは分かりますが、bだけがnullの場合にput2を呼び出すと、map.get("a")が無駄に変更されます。Javadocにはすべての副作用を明記するルールなので、これもJavadocに明文化することになります。
こういう場合はめんどうなので、フェイルファストで実装してやるとJavadocが短くなります。
public void put2(Object a, Object b) { if (a == null) throw new NullPointerException("a"); if (b == null) throw new NullPointerException("b"); map.put("a", a.toString()); map.put("b", b.toString()); }
これはJavadocで心がけることではなく、実装者側のプラクティスかもしれません。どちらにしろ、bだけがnullの場合に副作用が明記されていなければ、実装者はフェイルファストかトランザクションロールバックの仕組みを作ってアトミック性を保証しないと、実装とJavadocがずれます。
*1:最近では、abstractと書いていないものは基本的に継承しないのが普通だとは思いますが