mizoguche.info

KtorのコードリーディングでKotlinの知識を増やしていく - 拡張関数はUtilityクラス絶対殺すマン編

Ktorのコード読んでて、文法的にどうなってるか調べて説明できるようにするメモ。

一番簡単なサンプルコードでも一筋縄ではいかない。

ktor/HelloApplication.kt at master · Kotlin/ktor

fun Application.main() {
  // ...
}

拡張関数

継承やデコレーターパターンを使うことなくクラスの機能を拡張できる Extension Functions - Kotlin Programming Language

fun Application.main() { /* */ } は下記のような関数を定義している

Utilityクラス絶対殺すマン

val nullString: String? = null
StringUtils.isEmpty(nullString)  // => true

val emptyString = ""
StringUtils.isEmpty(emptyString) // => true

val spaceString = " "
StringUtils.isEmpty(spaceString) // => false

fun String?.isEmpty(): Boolean {
    if (this == null) {
        return true
    }
    
    return length == 0
}

val nullString: String? = null
nullString.isEmpty()  // => true

val emptyString = ""
emptyString.isEmpty() // => true

val spaceString = " "
spaceString.isEmpty() // => false

と書けて自然で読みやすくなるやつ。

Railsでアプリケーションつくったことあると、この手の機能はActiveSupportの日付・時間関連の拡張をはじめとしたあらゆるところで非常にお世話になったが、Kotlinでもこういうのが簡単に実装できるのは僥倖。

静的に解決される

Rubyのモンキーパッチではメソッドが動的に解決されるので、どのコードが実行されてるのかわからなくなるというのがあった。

Kotlinでは拡張関数は静的に解決される。IDEでどのコードが実行されてるか簡単に辿れるのでだいぶマシになりそうな印象を受けた。

レシーバーの型で解決する

open class C

class D: C()

fun C.foo() = "c"

fun D.foo() = "d"

fun printFoo(c: C) {
      println(c.foo())
}

printFoo(D()) // c

レシーバーの型によって解決されるため、printFooの引数の型 Cfoo() が実行される。

メンバー関数が勝つ

class C {
      fun foo() { println("member") }
}

fun C.foo() { println("extension") }

val c = C()
print(c.foo()) // member

常にメンバー関数が勝つので、オーバーライドされてなんやねんこの挙動! ということはなくなる。

スコープ

package foo.bar
 
fun Baz.goo() { ... } 
package com.example.usage

import foo.bar.goo

fun usage(baz: Baz) {
    baz.goo()
}

インポートしないと使えないので、知らんうちにメソッドが生えとる! ということはない。

が、IntelliJは優秀なので、あらゆるパッケージから拡張関数を探してきて補完してくれるし、補完から入力すると勝手にインポートする機能がついてるから実質知らんうちにメソッド生えてるということにはなる。

まとめ

しかし、オブジェクト指向の世界では、ユーティリティクラスはかなり悪い(酷いという人さえいるかもしれない)手法だ。 オブジェクト指向プログラミングにおいてユーティリティクラスに代わるもの | To Be Decided

データとメソッドを同じクラスにまとめるというオブジェクト指向の原則から外れるユーティリティクラスは死すべき🤘 みたいな過激派ではないが、その原則はかなり同意している。

ユーティリティクラスを絶対殺さなくてもいいけど、可読性・保守性が上がるなら拡張関数はどんどん使っていったらいいのでは、という印象。