2016年にJava8+JavaFXで作ったGUIツールをリファクタリングした


内容

背景

  • 2016年の7月にRPGツクールの非公式フォーラムにTKoolFacetileMaker2というツールを公開した
  • 4年も経った今はJavaも14になった
    • 仕事でJavaを書かなくなり、NimやGoへの興味関心が強くなった
    • 結果メンテを事実上放置していた
  • 4年経ってなお使っている人がちらほらいるのがメール通知から知っている
    • メンテナンスをする義務はないが、技術者として古くなったものをそのまま残しておきたくない気持ちが強くなってきた
    • ユーザにJava8をインストールしてもらうことが申し分けなくなってきた
    • 僕がこのツールを使うことはほぼないだろうけれど、重い腰をあげてメンテナンスすることにした

抱えていた問題

  • このツールを作った当時はまだ学生だったこともあり、色々作りに問題があった
  • 例えば
    • antという古いビルドツールでビルドしている
    • パッケージやクラス命名規則が定まっておらず、Javaプロジェクトで一般的でない
      • パッケージ名だとドメインの逆順がJavaでは一般的
      • そうなってなくて奇妙
    • MVCスタイルに則っておらず、ロジックが至るところに散らかっている
      • コントローラークラスにロジックが固まっていたりそうでなかったりする
      • 控えめに言って、クソ
    • プロジェクト構造がJavaプロジェクトの一般的な構造でなく、IntelliJに取り込めない
      • src/main/javaとかはない
      • resourcesがソースコードと一緒に存在する
    • テストコードがない
    • 依存ライブラリの一部のコードが残っておらず、そもそもビルドできない
      • 自作のオレオレライブラリに依存しているがオレオレライブラリのソースコードが残っていない
    • ビルド手順が残っておらずビルド方法がわからない
      • 依存ライブラリが行方不明で取得方法もわからずビルドの仕方がわからない
  • 2018年にも一度リファクタリングしようとして挫折した
    • gradleに移しつつkotlinに移植しようとして難しくてダレた
    • Oracle Java8からOpenJDK11に上げるとJavaFXがJDKから分離されたことでビルド方法が変わる
    • 色々あって挫折
  • 更に2年後、色々検討した結果、リファクタリングしてJavaのままメンテナンスを続ける方針に固まった

問題の分解

  • 前述の問題を分解して、一つ一つ潰していく方針にした
    • GitHubのIssuesに問題、やることを明確にした
    • いっぺんにやろうとすると死ぬ
    • 後でやるから今はやらないという割り切り
  • issues
    • gradleでビルドできるようにする
      • 最初は手動ビルドでいい
    • CIでビルドできるようにする
      • いつでもビルド/リリースできるようにする
    • コードフォーマットする
      • Googleコーディングスタイルに則る
    • Javaをアップグレードする
      • Java8からJava14へ
    • リファクタリングする
      • テストコードを書く前提にする
      • MVCにする
    • CIで自動テストする
    • テストコード整備する
    • ドキュメント整備する
      • 開発者向けのドキュメント(自分用)のを書く

大事にしたこと

  • とにかく「ビルドできる状態を維持する」が最優先
    • CIで自動ビルドできる状態を整えてからがスタート
    • 壊れたことを早期発見できないと死ぬ
  • リリースビルドをローカルPCで行なわない
    • 開発環境は変わりうる
    • CI環境でビルドしてリリースできれば、いつでもリリースできる
    • 何なら他人に引き継ぐことも可能
  • CIで自動テストする前提で実装する
    • UIとロジックを分離する
    • ロジック部分だけテストできるようにする
    • ロジックのモデル化

やったこと/できるようになったこと

2020/5/11 にようやく悲願を達成した

カスタムJREで動作可能になった

  • カスタムJREと一緒に配布しているので、ユーザPCに事前にJREをインストールしなくてよくなった
  • 同封の起動スクリプトをダブルクリックでGUIが起動する
    • Javaをインストールしていない環境で起動したときは感動した...
  • ファイルサイズは50MBほどになった
    • 結構大きいけれどやむなし

CIで自動ビルド、自動リリース

自動フォーマット

google-java-format-gradle-pluginでビルド前にフォーマットするようにした

テストカバレッジの可視化

  • JacocoというGradleプラグインでテストカバレッジを取得できるようにした
  • CodeCovでテストカバレッジを可視化した

コードのダイエット

  • 不要と判断した機能はばっさり削除したのでコード行数が大分減った
  • JavaFXに元々備わっているプロパティバインドという機能をフル活用した
    • これで値をセットしてまわらなくてよくなった

リファクタリング前

  • テストコードはなかった
$ find src/ -name '*.java' | xargs wc -l
       31 src/application/fileList/FileListHBox.java
      189 src/application/fileList/FileListHBoxController.java
       31 src/application/imageViewer/ImageViewerBorderPane.java
      288 src/application/imageViewer/ImageViewerBorderPaneController.java
       92 src/application/Main.java
      471 src/application/MainController.java
       44 src/application/options/Numberings.java
      104 src/application/options/Options.java
       40 src/application/options/OptionsStage.java
      165 src/application/options/OptionsStageController.java
       69 src/application/options/Separators.java
       36 src/application/outputViewer/MyButton.java
      122 src/application/outputViewer/MyImageView.java
       33 src/application/outputViewer/OutputViewerAnchorPane.java
      130 src/application/outputViewer/OutputViewerAnchorPaneController.java
       25 src/application/TKoolVersion.java
       29 src/application/version/VersionStage.java
       42 src/application/version/VersionStageController.java
     1941 total

完了後

  • テストケース数は90になった
# main
$ find src/main/java/ -name '*.java' | xargs wc -l
         8 src/main/java/com/jiro4989/tkfm/data/CropSize.java
        42 src/main/java/com/jiro4989/tkfm/data/Position.java
        38 src/main/java/com/jiro4989/tkfm/data/Rectangle.java
        67 src/main/java/com/jiro4989/tkfm/Main.java
       422 src/main/java/com/jiro4989/tkfm/MainController.java
       235 src/main/java/com/jiro4989/tkfm/model/CroppingImageModel.java
        21 src/main/java/com/jiro4989/tkfm/model/ImageFileModel.java
        52 src/main/java/com/jiro4989/tkfm/model/ImageFilesModel.java
       211 src/main/java/com/jiro4989/tkfm/model/PropertiesModel.java
       133 src/main/java/com/jiro4989/tkfm/model/TileImageModel.java
        15 src/main/java/com/jiro4989/tkfm/util/ImageUtil.java
         6 src/main/java/com/jiro4989/tkfm/Version.java
      1250 total

# test
$ find src/test -name '*.java' | xargs wc -l
        13 src/test/java/com/jiro4989/tkfm/data/CropSizeTest.java
        29 src/test/java/com/jiro4989/tkfm/data/PositionTest.java
        26 src/test/java/com/jiro4989/tkfm/data/RectangleTest.java
       212 src/test/java/com/jiro4989/tkfm/model/CroppingImageModelTest.java
        28 src/test/java/com/jiro4989/tkfm/model/ImageFileModelTest.java
        64 src/test/java/com/jiro4989/tkfm/model/ImageFilesModelTest.java
       143 src/test/java/com/jiro4989/tkfm/model/PropertiesModelTest.java
       120 src/test/java/com/jiro4989/tkfm/model/TileImageModelTest.java
        43 src/test/java/com/jiro4989/tkfm/util/ImageUtilTest.java
        13 src/test/java/com/jiro4989/tkfm/VersionTest.java
       691 total

テストコードの整備

  • 前述の通りロジック部分のテストカバレッジを90%以上にした
  • JavaFXのテストをするためにTestFXプラグインを導入した
    • CI環境だとDISPLAYがないのでテストがコケる
      • xvfb-run というコマンドで回避
  • JUnit5を使った
    • Goだとテーブルドリブンテストを書くので同様のテストを書いた
    • Junit5のParameterizedTest便利

かかった時間

  • 設計の時間も含めて66時間かかった
    • たかだか2000行のプログラムに66時間もかかると思ってなかった...
    • GWが全部消えた+GW終わった後もやってた

苦戦したこと

自分が作ったツールに自分で苦しんでるとかバカじゃねえの(嘲笑)

gradle

  • そもそもgradleを書いたことがなかった
    • 書き方や概念を理解する必要があった
  • mavenは書いたことあるのでmavenでも良かったが、スクリプト言語のが好みなのでgradleにした

JavaFX分離問題 (jmods)

  • Oracle Java8のころはJDK内にJavaFXが含まれていた
  • Java9以降でJavaで無料で開発するにはOpenJDKを使う必要がある
  • OpenJDKも同様にJavaFXはJDK内に含まれていない
  • JavaFX SDKとJavaFX JMODSを使用してモジュール機構を使う必要がある
    • Java8で書いていた当時には存在しなかったモジュール機能の理解に苦戦

カスタムJRE

オレオレライブラリのソースコードがない

  • 自分で配布していた実行可能JARからクラスファイルを抜いてきてjadでデコンパイルして生成されたコードから復元した
    • 自分の配布物をデコンパイルする日がくるとは...

感想

やってよかった

  • 機能的には増えたどころかむしろ減った
    • けれど保守性は上がった
  • 自動テストできるようになった
  • いつでもリリースできるようになった
  • Java8を使い続けないといけない呪縛から開放された
    • Java14にできて良かった

4年前の反省

  • 使わない機能は実装しない
    • 保守コストが増加するだけで、良いことなし
  • UIとロジックの密結合は避ける
    • 画面を手で操作しないとテストできない状態は良くない
      • テスト自動化を阻害する
  • テストコードを書こう
    • 正しい挙動がコードに残る
    • 考慮していなかった振舞いに気付ける
    • テストを書く前提の実装になる
      • 品質が向上する
  • テストカバレッジをとろう
    • CodeCovで可視化したことでテストしていない分岐を発見できた
  • 自動テスト環境を作ろう
    • 今回はまっさきに自動ビルド/テスト環境を[GitHubActions]で構築したが正解だった
    • 挙動が変わったことにすぐに気づける
    • 自動テスト環境は早く構築すればするほど効果でる
  • 自動リリース環境を作ろう
    • ビルド/リリース方法は忘れる
    • リリースできる環境は変わる
      • ホストPCはOSレベルで変わる
      • ホストPCだけでビルドしてると「気づかない依存」をしているかもしれない
        • 今回だと「ソースコードの残っていないオレオレライブラリ」がまさにそれ
          • 当時はビルドできたけれど今は出来ない
        • CI環境でリリースできるようにしていれば、依存がかならず何処かにコード化される
        • 誰でも何時でもビルド/リリースできるようにCI環境上に構築しておくべき
  • 4年前の僕の実装はクソだった
    • ごみ
    • ばか
    • うんこ

今後

  • このツール以外にもJavaFXで作ったツールがいくつかあるので、同様のリファクタリングをしていきたい
  • (ユーザほとんどいないので、ただの自己満足だけど)

まとめ

  • 最初からちゃんと設計しよう
  • テストはちゃんと書こう
  • 自動化しよう
  • デファクトスタンダードに従おう

最初からこれらをやっていれば、こんな辛い思いはしなくてすんだのに...

以上