Kengo's blog

Technical articles about original projects, JVM, Static Analysis and JavaScript.

コード内コメントとJavadocの書き方

社内勉強会向けに作成したコード内コメントとJavadocの書き方についてのスライドを、Speaker DeckとSliDeckで公開しました。

このスライドは概要とメリットについてざっくり説明し理解と学習の動機を作ることを目的としていますが、これは日本ないし中国の大学ではコメントについてそこまで扱わないらしい*1という前提に基づいたものです。

ここに記載のない発展的な内容としては以下が挙げられますが、これらについて学ぶのはまず手を動かした後で良いと考えています。

プレゼン後に「publicでないクラスやメソッドJavadocを書くべきか?」という質問を受けましたが、私の答えは「書くべき」です。自分自身の理解を深め、備忘にもなり、将来のメンテナンスも助けてくれます。少しでも直感的でないコードがあるならば、時間を取って書きたいです。 まぁ書くべきものは厳密には対外的なAPI Specではなく、内部実装方針や歴史的経緯等になるかもしれませんので、Javadocを書くべきというよりは開発者向けドキュメントを書くべきと言うべきなのかもしれません。

また、ドキュメンテーションコメントはOSS contributionの良い題材になるとも話しました。typo修正くらいなら敷居は低いですし、多くのOSSでは喜ばれるはずです。自分の経験上、リジェクトされることはほとんどありませんでした:

なお自分のJavadocに関する理解は、Oracle公式ドキュメントに加えて「エンジニアのためのJavadoc再入門講座」に依るところが多いです。英語の解説書でこのくらい幅広く丁寧に説明したものが欲しい……。

エンジニアのためのJavadoc再入門講座 現場で使えるAPI仕様書の作り方

エンジニアのためのJavadoc再入門講座 現場で使えるAPI仕様書の作り方

*1:共同開発ないし保守については優先度が低い印象を受ける

(メモ)クラスタ管理関連

  • etcd
    • KVS
    • Leader/Follower
    • Raft
  • fleet
    • Deploy
    • Service Discovery
  • Hashicorp Consul
    • CP
    • needs several nodes as server per Datacenter
    • each nodes in Datacenter installs Consul agent
  • Hashicorp Serf
    • AP
    • is used by Consul
  • Docker Compose
    • Define set of container instances
  • Docker Swarm
  • Docker Machine
    • Create and initialise instance to deploy Docker container
    • Supports both remote and local
  • Registrator
    • Hook for docker command, to register/unregister container instances
  • Netflix Eureka
    • Service Discovery
  • SkyDNS2
    • CP
    • DNS but not only address but also ports
    • Depends on etcd
  • Hashicorp Terraform
    • Deployment
  • Hashicorp Otto
  • Hashicorp Packer
  • Netflix Asgard
  • systemd
  • Sensu

Guava Release19がそろそろ来そう

現時点でRC2まで出ています。

個人的に興味深いのはCharMatcherにファクトリメソッドが導入された理由。staticフィールドだとクラス初期化時に初期化されなければならないが、ファクトリメソッドならメソッド実行時までインスタンス生成を遅延できるためと説明されています。

// before
  public static final CharMatcher WHITESPACE =
      new NamedFastMatcher("WHITESPACE") {
          :
          :
          :
      };
// after
  public static CharMatcher whitespace() {
    return Whitespace.INSTANCE;
  }

  @VisibleForTesting
  static final class Whitespace extends NamedFastMatcher {
    static final Whitespace INSTANCE = new Whitespace();
    :
    :
    :
  }

なおこうした内部クラスを利用する方法*1はマルチスレッド環境でもsynchronizedや明示的なロックが必要ないことで知られています*2。 Stringでもprimitive型でもない定数、特に正規表現のような初期化処理が重いものは、積極的にファクトリメソッドで置き換えると良さそうです。

*1:このページのlazy initialization holder class idiom

*2:だからこういう問題も起こるのですが

SLF4Jの使い方をfindbugsで自動検証する

今日、拙作findbugs-slf4jGitHub Pageを作りました。このブログではこのプロジェクトについて日本語で紹介したいと思います。

概要

  • 拙作PMDプラグインをもとに、2012年8月から実装を開始したfindbugsプラグイン
  • SLF4J利用時のよくあるトラブルを未然に防ぎ、コードの品質を担保する
  • 全自動なので、開発者各々の個性に影響されずに品質の底上を実現できる

既存の課題:SLF4Jの直感的ではないインタフェース

SLF4J(Simple Logging Facade for Java)はJava向けに開発された、ログ用のFacadeです。開発者はSLF4Jが提供するインタフェースを利用することで、「高パフォーマンスなログ処理」と「利用者に好きなログ基盤を選ばせる自由」を提供することができます。例えばLog4j 2のような新しいログ基盤でも、プログラムがSLF4Jを使っていれば、プログラムには手を加えること無く利用が可能です。

ただしSLF4Jのインタフェースはやや独特で、学習と慣れが必要です。例えば下記に挙げる例ではexample 1のほうが直感的ですが、SLF4Jではexample 2の方を推奨しています。この方法はパフォーマンス上有利なのですが、コードに長年手が加えられていくとプレースホルダとパラメータ数の不一致などが容易に発生するため、ログに残すべき値が残せていなかったなどのトラブルに繋がります。

// example 1: not good, because JVM will concat strings even if debug log is needless
logger.debug("User id is: " + userId);

// example 2: better, because it will not generate needless string if debug log is needless
logger.debug("User id is: {}", userId);

また例外のstacktraceを出力するためのメソッドinfo(String format, Object... arguments)だというのもわかりにくい点です。以下の例のように可変長配列の最後の要素がThrowableであればstacktraceを出力してくれるのですが、これはドキュメントを読むか人に聞くかしなければわからないでしょう。

// example 3: not good, because stacktrace and log messages are separated into two log records.
// And user cannot configure how to log stacktrace.
logger.warn("Exception ({}) occurred when userId is {}", e.getMessage(), userId);
e.printStacktrace();

// example 4: better, it's configurable and not separated into multiple log records.
logger.warn("Exception occurred when userId is {}", userId, e);

SLF4Jは現在のJava開発でデファクトスタンダードと言えるログ用ライブラリですが、このような難しさがあるため利用時は経験ある開発者からのサポートやレビューが欠かせません。ただし、アプリケーションコードのどこにでも書かれうるログ処理のすべてを人力でレビューし品質を担保するのは至難の業です。

提案する解決:静的解析による自動的かつ横断的な検証

私が開発したfindbugsプラグインの発想はごくシンプルなもので、上記で挙げた問題を機械による検証によって解決するというものです。 個人の経験と尽力によって回避するのではなく、検証を機械化し自動化することでコストの削減とスケーラビリティの向上、抜け漏れ防止を実現します。

技術的な特徴

主な技術としては、findbugsが提供するオペランドスタック解析を主に使用しています。これによりプレースホルダとパラメータの数の不一致を検出したり、e.getMessage()をパラメータとして使っているコードを検出したり、LoggerFactory.getLogger(Class)メソッドの引数を検証したりしています。また.classファイルに書かれた定数文字列を解析することで、簡易ではあるもののログフォーマットの人間にとっての可読性を検証するといったことも取り入れています。

findbugsプロジェクトは息が長いということもあり、コードが非常に難解です。またテストも困難です。 このプラグイン開発では統合テストを重視テストパターンを増やすことで、TDDを可能にしつつエンバグを減らすことに努めています。またバグ修正時はまず再現テストを書く、ということを徹底しています。

またTravis CIが提供する機能により、Java 7&8SLF4J 1.6&1.7の2軸でビルドマトリックスを組んでいます。これも多くの環境をサポートする上で非常に役立っています。

どのくらい使われているか

OSSプロジェクトでの利用実績はほとんど無いと認識していますが、クローズドなプロジェクトではそこそこ使われているようです。2012年11月にいただいたIssueをはじめとして、今でもバグ報告・新規機能要望などをいただきます。「このコードを解析するとエラーになる」と、.javaファイルや.classファイルをメールでいただくこともあります。

特にこの5月ごろから月あたりのDL数が平均40から1,700(ユニークIPアドレスベースでの計数)へと大幅に上がったのですが、どこかの大きなプロジェクトで使っていただいているのでしょうか。

f:id:eller:20150920134539p:plain

以上です。 もし皆さんのプロジェクトでSLF4Jを使っていたら、ぜひ当findbugsプラグインの利用を検討してみてください。Mavenで使う方法はこちらに書いてあります

関連する記事

How I find and learn new technical things

Sometimes my teammates ask me about how to find and learn new technologies. It would be common question for technologist and developers, so I will try to summarise current my theories.

How to find new technologies

RSS (articles)

RSS is still my best method to collect information from WWW. I have already tried AI based curation like Gunosy and SmartNews, but they summarise information from only limited sites. To collect information from everywhere, RSS is the best way for now.

I use feedly pro to summarise information and check it everyday morning. Its free version is also good and enough, please try.

Here I will list several sites from my subscriptions:

e-book

RSS is good to find new things, but it might not enough to learn new things because each article is small and not well organised. Best solution is the book, it is designed for my usage; It is well packaged so I can find all related information and background.

So when I want to learn some specific topic like BLE, ROS, Arduino or Scala, I visit following sites to purchase e-book.

GitBook and similar sites have potential to be good place to find book, but in many cases, books are not edited.

SNS

I do not use SNS so much for now, because now my network environment is not good. But following sites are good, they have many information what I do not know.

Mailing list

Many OSS projects have mailing list. When you get interested in OSS, I recommend you to find mailing list and join it.

And there are also mailing list which introduce new information continuously.

Meetup

Meetup is good place to meet with new information. You may want to check:

Pocket

Pocket might be the most important tool to collect information. It helps me to postpone reading when I have no time to with with care.

How to make time to learn

Even though you have method, it does not work without time to use. So most important thing is that making time to learn continuously.

Basically I have good concentration at morning, so I make time to learn at morning. If you are also, then please try to make habit and environment to read articles at train, bus, desk or somewhere.

Googleのアサーション用ライブラリTruthを試してみた

Guavaのテストコードを読んでいたらTruthというtesting frameworkが使われていることに気づき、最新の個人プロジェクトで使ってみました。まだアルファ版ですし、自分でも使い続けるかどうか微妙なところですが、試用記録として利点をまとめます。

なお著者がアサーションフレームワークに求めるのは、大人数が関わるプロジェクトにおける「開発者の個性(経験、知識、趣味)に限らず、短時間で保守性が高く直感的なコード・エラーメッセージが書ける」ことです。異なる観点からこのプロダクトを見ると、また違った意見があるかと思います。

assertThat()が必要とされた理由

そもそもassertThat()はなぜ必要なのでしょうか。それはassertTrue(), assertFalse() などのメソッドが生むエラーメッセージが直感的でないからです。

Truthのウェブサイトにのっている例が非常にわかりやすいです。直感的なメッセージにするには、結局文字列で状況を説明する必要があります。この方法は開発者の個性に依存してしまいますし、忙しい時などに文字列を書き漏れる恐れもあります。

List <Entity> entityList = service.search(condition);
assertTrue(entityList.isEmpty());
// -> AssertionError

assertTrue("No entity should be found under this condition", entityList.isEmpty());
// -> AssertionError: No entity should be found under this condition

JUnit4はこの問題に対してassertThat()という解決を用意しています。これによって、比較的可読性の高いメッセージを得ることができます。

List <Entity> entityList = service.search(condition);
assertThat(entityList, is(empty()));
// -> AssertionError: Expected: is an empty collection  but: <[unexpected entity]>

assertThat()には確認する値と、期待する状態を表すMatcherとを渡します。Matcherには様々なものが用意されており、ほぼすべての用途をカバーできると考えられます。

なおassertThat()を利用したコードには「英語として読んで意味が通じる」という利点もありますが、Truthでもそこは変わらないためここでは割愛します。

既存手法の持つ問題

ではなぜ代替が必要なのか。私は状況・目的に応じたMatcherを開発者が知らなければならないことが大きな問題と考えます。

例えばCoreMatchersMatchersだけでも、以下の記事で紹介されている通り様々なMatcherがあります。この他にJUnitMatchers(JUnit4.4以降)もあります。これらを知らないと、assertThat(age < 30, is(true)) のようなエラーメッセージ生成に役立たないMatcherの使い方をしてしまいかねません。

他にもassertThat()で生成されたエラーメッセージには「どの値が」問題であったか(上記の例で言うと「entityListが」問題だった)という情報が含まれておらず、結局テストコードを読まなければどんな処理がどのような理由で失敗したのかがわかりません*1IDE単体テストをガンガン回すテスト駆動開発であれば問題になりませんが、テストが落ちた時の対応はコスト高です。

Truthがもたらす解決

Truthはこの問題に対して、メソッドチェーンとnamed()を導入することで解決を図っています。

Truthは期待する状態を表すコード(前述のMatcherに相当)を、引数でなくメソッドチェーンにて記述します。 メソッドチェーンにより、assertThat(...)を書いた時点で、利用可能なメソッドIDEにより一覧表示されます。開発者はすべてのMatcherとその用途を記憶する必要がありません。必要なもののみが、必要なタイミングで、表示されます。

assertThat(someInt).isEqualTo(5);
assertThat(someCollection).contains("a");
assertThat(aMap).containsKey("foo")

また「何が」期待通りでなかったかを補足するためにnamed()メソッドを提供します

// Reports: "hasError()" is unexpectedly false
assertThat(myBooleanResult).named("hasError()").isTrue();

メッセージそのものを指定するwithFailureMessage()も用意されていますが、個人的にはこちらは使わないで良いのではと思います*2

他にもMultimapのようなGuavaが提供するCollectionフレームワークにデフォルトで対応していたり、自分で新しい型に対応させられたりCollectionの要素に対するAssertionを書けたりと、細かいところで気が利いています。

Truthの問題点

以上、良い所も多いプロダクトですが、最近はさほど活発に更新されてはいないようです。アルファ版と言え枯れつつあるからなのか、まだユーザが少ないのかは不明ですが、Mavenリポジトリの情報を見るとまだ利用数が少ないため後者ではないかという印象です。

またアサーションフレームワークには他にも有名なものがいくつかありますが、それらとの違いに関する公式ドキュメントは現時点ではありません。ドキュメント以外では一応、Issueのコメントに拡張性に違いがあると述べられています。Truthが”very alpha”だという点が気になるようであれば、AssertJが良い選択肢になるかもしれません。

以上、なかなか魅力的なプロダクトだと思いますがいかがでしょうか。

*1:1テストメソッド1アサーションを貫けるなら、あるいは……

*2:そもそも私の関心は「技術者の個性に左右されないテストメッセージの生成」にあるので