mike-neckのブログ

Java or Groovy or Swift or Golang

Interface に対してプログラミングする

interface に対してプログラミングするのがベストプラクティスであるとする考え方が、 Java のプログラミングにあるようです。

依存関係を切断・逆転できて便利とか、インターフェースが変わらない限りは実装が変更されても利用するクラスは変更されないなどのメリットがあるんじゃないでしょうか。

オブジェクト指向のなんたるかを論じるほど僕はデキたプログラマーではないので、かわりに Gradlle を使ってビルドするときにどれくらい嬉しいのかを書いていきたいと思います。

なお、これはつい先日(2020/12/11)に行われた JJUG ナイトセミナー Maven/Gradle ビルドツール特集のときにさらりと話した部分の補遺です。

f:id:mike_neck:20201219214441p:plain

理屈としては、上の画像にあるように、インターフェースを使って実装を分離している場合、

実装(この図では UserRepositoryImpl クラス)を変更した際に、そのインターフェースである UserRepository を使う UserService クラスは何ら影響を受けません。

その場合、 Gradle はこの UserService クラスのコンパイルが不要であると判断して、前回のコンパイル結果をそのまま使います。

その結果、ビルドの速度が速くなるというのが interface に対してプログラミングすることの Gradle における恩恵になります。

UserService クラスが UserRepository を介さずに直接 UserRespositoryImpl クラスを参照・呼び出ししている場合には、たとえ UserServiceJava コードを変更していなくても UserRepositoryImpl に変更があれば UserService クラスの再コンパイルも必要になります。

f:id:mike_neck:20201219215243p:plain


では、どれくらい時間に差が出るのか気になったので、サンプルで 15,000 ファイルくらいのプロジェクトを作って計測してみました。

結果を先に述べると、インターフェースありのプログラムのほうがコンパイル時間が短くなります。


実験手順

  1. 一番最初の画像にあるような FooId <- Foo <- FooRepositoryImpl -> FooRepository <- FooService というインターフェースを挟んだシンプルな参照関係(参照する側 -> 参照される側)のある Java ファイルが 15,000 組(合計 75,000 ファイル)ほどある Java プロジェクトを作る(Foo 部分の名前がランダムに生成した名前(例えば OnPerfections)に変わる)
  2. 1.のプロジェクトをビルドする(ここでは compileJava タスクを実行する)
  3. 15,000 組の中から 60 組ほどランダムに選び、実装クラスである FooRepositoryImpl のインターフェースに影響の出ない部分のコードを変更する
  4. 改めてビルド(compileJava タスクを実行)する
  5. 4.のインクリメンタルビルド時間を取得する
  6. 1.の関係でインターフェースを挟まないようにしたプロジェクト(FooRepository を作らず、 FooService が直接 FooRepositoryImpl を参照している)を作成する
  7. 6.に対して上記の 1. 〜 5. を実施、 5. の結果をインターフェースのある場合と比較する

なお、実験環境は以下の通り

  • プロジェクトにはビルドキャッシュを適用する
  • github actions の ubuntu:20.04 で実行する

実験結果

回数 インターフェースあり・初回(参考) インターフェースあり・編集後 インターフェースなし・初回(参考) インターフェースなし・編集後
1 17.713 4.66 20.361 6.266
2 19.228 4.93 20.495 6.331
3 20.524 5.214 19.53 6.517
4 23.933 5.958 18.229 6.086
5 19.598 5.14 21.349 6.636
平均 20.1992 5.1804 19.9928 6.3672

上記の結果から、 インターフェースありのプログラムは 60 ファイル程度の編集でインクリメンタルビルドでの時間が 1 秒程度速くなることがわかった。


感想的なあれ

  • 参照関係がシンプルであったために、期待していたほどコンパイル時間に差が発生しなかった
    • もう少し参照関係が複雑なプログラムを作成して比較したほうが良さそう
  • ビルドキャッシュを設定しなかった場合も調べたほうが良さそう
  • 今回の計測には github actions を使ったがちゃんと AWS などでバーストしないタイプのインスタンスで比較したほうが良さそう

インターフェースに対してプログラミングする場合に、わずかながらインクリメンタルビルドの時間が短くなることが示せたが、 1 秒程度の差なら気にせずクラスを直接参照するみたいな考え方もあるだろうと思われる

FooService 側が複数の Repository 的なインターフェース・クラスを参照するより現実的なプログラムであった場合に、より多くの差が発生するかもしれないので、もう少し実験してみたいとは思う