コード(レガシーコード)の移行は楽しいものではありません。移行には膨大な計画と努力が必要です。開発者にとって最もエキサイティングでモチベーションの上がる仕事ではないが、レガシーコードを新しいライブラリバージョンに移行するには、決意と適切な経験が必要だ。Joda-Timeからjava.timeへの移行は、綿密な計画と実行を必要とする移行の一つである。
もしあなたのJavaプロジェクトがJava SE 8以前に開始され、日付/時刻処理を使用しているなら、おそらくJoda-Timeを使用しているでしょう。Joda-Timeは優れたライブラリであり、SE 8以前の日付/時刻機能を処理するためのデファクトスタンダードです。
Java SE 8では、新しく改良された標準的なDate and Time API、通称「java.time (JSR-310)」が導入されました。Joda-Timeプロジェクトでは、java.time (JSR-310)への移行を推奨しています。
java.time(JSR-310)はJoda-Timeに大きな影響を受けていますが、下位互換性はなく、概念や用語も変わっています。そのため、Joda-Timeからjava.timeへの移行には、変更するコードの1行1行に注意を払う必要があります。これは時間のかかる作業であり、もっと簡単で自動化された移行方法があればと思ってしまいます。
私たちは、Sensei を使用してそれを作成しました。これは、お客様が定義したレシピ(ルール)に従ってコード変換を自動的に実行する IntelliJ プラグインです。再利用可能なレシピを定義することに時間を使い、反復的な移行作業を行う必要はありません。この自動化は、レガシーのJoda-Timeコードを変換するだけでなく、チームがIDE上でガイドラインに従って新しいコードを書くのにも役立ちます。
皆様のお役に立てるよう、Joda-Timeからjava.timeへの移行をより簡単に行うためのレシピを掲載した公開Sensei クックブックStandardization on java.time (JSR-310)を作成しました。これは、Joda-Timeからjava.timeへの移行をより簡単にするためのレシピが含まれており、今後もレシピを増やしていく予定です。
ここでは、Sensei がいかにレガシーコードの移行を容易にするかを理解していただくために、サンプルの移行例をご紹介します。
反復的な手動移行から自動化されたコード変換へ
Joda-Timeからjava.timeにコードを1行移行する際に、いくつかの隠された罠を示す、新しいDateTimeの作成例を見てみましょう。次に、Standardization on java.time (JSR-310) cookbookからSensei レシピの一つを見て、どのようにこれらの情報を取得するかを紹介します。そうすることで、どの開発者でも同じ移行を繰り返し利用することができます。
この例では、DateTimeフィールドの値を表す7つのint型引数からJoda-TimeのDateTimeを作成しています。
これをjava.timeと同等のものに移行するにはどうしたらいいでしょうか?
このコンストラクタのJoda-Timeのjavadocにはこう書かれています。
デフォルトのタイムゾーンでISOChronologyを使用して、datetimeフィールドの値からインスタンスを構築します。
まず、java.timeの中にDateTimeクラスがあると思いがちですが、そんなものはありません。migrate from joda time to java time」でググると、Stephen Colebourne氏のブログ記事「Converting from Joda-Time to java.time」がヒットします。
これで良いスタートが切れ、java.time.ZonedDateTimeやjava.time.OffsetDateTimeを使う方向になりました。ここで最初の質問ですが、どちらを使えばいいでしょうか?Stephenのコメントから、おそらくZonedDateTimeだと思います。
ZonedDateTimeのjavadocを見ても、コンストラクタが全く見当たりません。Stephenのブログ記事に戻り、さらに下を読んでみます。
コンストラクションjava.timeにはファクトリーメソッドしかないので、文字列用のparse()メソッドは用意されていますが、変換はユーザーの問題です。
そこで、スタティックなファクトリーメソッドがあるはずです。スタティックメソッドを検索すると、似たようなものが見つかりますが、正確には同じではありません。
Joda-TimeのDateTimeコンストラクタと同様に7つのint型パラメータがありますが、注意して見ないと重要な点を見逃してしまいます。 これはJoda-Timeよりもjava.timeの方が精度が高く、ナノ秒単位で瞬間を測定するからです。これは、java.timeがJoda-Timeよりも精度を上げ、瞬間をナノ秒単位で測定するようになったためです。さらに、このメソッドはZoneIdを要求しますので、今までなぜZoneIdを必要としなかったのか、そして今なぜZoneIdを必要としているのかが気になるところです。
オリジナルのコンストラクタのjavadocには、デフォルトのタイムゾーンを使用すると書かれていましたが、デフォルトのZoneIdを取得する方法があるのでしょうか?
ZoneId の javadocにはコンストラクタが記載されていませんが、スタティック・メソッドを見ると、systemDefault()が使用できることがわかります。
さて、ZoneIdを整理したところで、ミリ秒からナノ秒への変換はどうすればいいのでしょうか。java.util.concurrent.TimeUnitを使って変換してみましょう。
このメソッドはlongを返しますが、私たちのメソッドはintを期待しているので、変換問題も解決しなければなりません。何か簡単なことをやってみましょうか。掛け算ですか?
これは動作しますが、少し違和感があります。すでにお気づきではないかもしれませんが、たった1行のコードを移行するために、かなりの時間と労力を費やしています。しかし、ご想像のとおり、このような編集を手作業で何度も行っており、なかなか改善されません。
しかし、java.time APIをもう少し詳しく見てみると、もう少し流暢に見える解決策を見つけることができます。
ZonedDateTimeにはミリ秒を設定する明確な方法はありませんが、ChronoField.MILLI_OF_SECONDをTemporalFieldとして使用し、with(TemporalField field, long newValue)メソッドを使用して設定することができます。
また、javaのドキュメントには、ナノ秒への変換を行ってくれると書かれています。
このフィールドで値を設定する場合は、NANO_OF_SECONDに1,000,000を乗じた値を設定するのと同様の動作になります。
そこで、ファクトリーメソッドでナノ秒に0を指定し、withメソッドで元の値とミリ秒の両方を持つZonedDateTimeを作成するだけでよいのです。
最終的な結果を見ると、たった1行のコードを変更しただけのように見えますが、これではたった1回のマイグレーションに費やした労力を示すことはできません。
レシピを作成することで、より早く、より簡単に移行することができます。
Sensei は、この苦労して得た情報を他の開発者と共有するための方法を提供しています。これらの要件をすべて把握したレシピを作成することで、Sensei のユーザーは、マウスをクリックするだけでこの移行を実行できるようになります。
Sensei レシピは主に3つのセクションで構成されています。
この呼び出しをjava.timeに相当するものに移行するためのSensei レシピ(YAMLレシピとも言えます)を見てみましょう。
DateTime foo = new DateTime(year, monthOfYear, dayOfMonth, hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
メタデータセクション
メタデータセクションには、レシピとその使用方法に関する情報が含まれています。
検索セクション
Sensei レシピの検索セクションでは、このレシピがどのコード要素に適用されるかを指定します。
search:
instanceCreation:
args:
1:
type: int
2:
type: int
3:
type: int
4:
type: int
5:
type: int
6:
type: int
7:
type: int
argCount:7
type: org.joda.time.DateTime
この検索セクションでは、私たちは
- instanceCreation(コンストラクタの使用法)を検索します。注:この他にも様々な検索対象があります。
- コンストラクタには7つの引数が必要で、これはargCountプロパティで指定します。
- 引数1~7はint型であること
- org.joda.time.DateTimeタイプのコンストラクタを探しています。
修正プログラムのページ
availableFixesセクションでは、一致するコード要素に適用可能な1つまたは複数の修正を指定できます。各修正は複数のアクションを持つことができ、今回のケースでは、2つのアクションを実行する1つの修正があります。
- クイックフィックス」メニューには、ユーザーがこのクイックフィックスを適用した場合に何が起こるかを示す修正プログラムの名称が表示されます。
- アクションリストには、このクイックフィックスで実行されるアクションが表示されます。
- rewriteアクションは、mustache テンプレートを使用してコード要素を書き換えます。変数や文字列置換関数を利用することができます。
- modifyAssignedVariableアクションは、このコンストラクタが変数への値の割り当てに使用されているかどうかを確認します。使用されている場合、このアクションは、変数がtype
レシピを使ってコード変換を行う
レシピが書き込まれて有効になると、コードをスキャンして適用可能なセグメントをハイライトします。
以下のスクリーンショットでは、対象のコンストラクタがSensei によってマークされていることがわかります。マークされたコンストラクタにカーソルを合わせると、Recipe shortDescriptionとQuickfixオプションMigrate to java.time.ZonedDateTimeが表示されます。
java.time.ZonedDateTimeへの移行」というクイックフィックスを選択すると、レシピで指定したアクションに応じてコードが変換されます。
一度だけの移行で、チーム間で統一されたコーディング作業が可能になります。Sensei
Sensei は、その知識を実用的なレシピやクックブックにして、チーム内で共有することができます。一度だけの移行スプリントを計画することもできますし、Joda-Timeのコードに出会ったときにjava.timeに瞬時に変換するという漸進的なアプローチをとることもできます。移行を論理的な段階やステップで行う方法として、レシピを有効/無効にしたり、Sensei でスキャンするファイルの範囲を拡大/縮小したりすることができ、コードの移行を苦痛に感じさせない柔軟性があります。
ライブラリの移行は、プロジェクトを標準化するためにSensei を利用できる多くの方法の一例に過ぎません。アンチパターンや、プルリクエストや自分でコーディングしているときに頻繁に遭遇する手動のコード変換を常に探しておくことができます。開発者が見落としがちなコーディングガイドラインがあれば、そのガイドラインをレシピに変換して、開発者が承認されたコード変換を自信を持って適用できるようにすることができます。
ご質問があれば、ぜひお聞かせください。sensei-scw.slack.comのSlackにご参加ください。