翻訳: Kotlinベストプラクティス『Idiomatic Kotlin. Best Practices』

  • 49
    いいね
  • 0
    コメント

この記事について

Philipp Hauer's Blog
Idiomatic Kotlin. Best Practices.
https://blog.philipphauer.de/idiomatic-kotlin-best-practices/

この記事はKotlinらしくKotlinを書くベストプラクティスが書かれており、とても参考になります。
許可をいただいたので、翻訳させていただきます。

もし間違えやもっと良い翻訳などあれば編集リクエストかtakahiromまでお願いします。


Idiomatic Kotlin. Best Practices

kotlinを最大限活用するために、Javaにおけるベストプラクティスを考え直す必要があります。Javaのベストプラクティスの多くはKotlinに提供されている機能によって置き換える事ができます。Kotlinらしい(Idiomaticな)Kotlinを書いて、Kotlinのやり方を見ていきましょう。
image.png

警告の言葉 : 以下のリストは網羅的ではなく、また私の控えめな意見を言っているだけです。さらにいくつかのKotlinの機能は健全な判断によって使われるべきです。もし使いすぎた場合、コードが読みにくくなることもあります。例えばあらゆることを一つの読めない1文に短縮することによって、“train wreck”を引き起こします。

KotlinでサポートされているJavaらしい書き方やパターン

Javaにおいて、特定のイディオムやパターンを実装するためにたくさんの定形コードを書く必要があります。運の良いことにたくさんのパターンはKotlinの言語やその標準ライブラリによって実装することが出来ます。

Javaのイディオム・パターン Kotlinでの書き方
Optional Nullable型
Getter, Setter, Backing Field プロパティ
Static Utility Class トップレベル(拡張)関数
(Top-Level(extension) function)
Immutability(不変性), Value Objects(値オブジェクト) immutableプロパティを使ったdata class, copy()
Fluent Setter(メソッドチェーンで行うSetter) 名前付きのデフォルト引数, apply()
メソッドチェーン デフォルト引数
シングルトン object
デリゲート デリゲートプロパティ by
遅延初期化 (スレッドセーフ) デリゲートプロパティ by: lazy()
オブザーバーパターン デリゲートプロパティ by: Delegates.observable()

関数型プログラミング

他の利点の中で関数型プログラミングは副作用を減らすことができ、それにより以下のようなコードを作成することができます。

  • 誤りがちな表現を防げる
  • 簡単に理解できる
  • テストが書きやすい
  • スレッドセーフ

また、Java8と対比して、Kotlinは関数型についてもっと良いサポートを行っています。

  • 不変性(Immutability) : 変数とプロパティのval, 不変な data class, copy()
  • 式(Expressions) : Single expression function, if, when そして try-catchは式です。
    これらの制御構造を他の表現と簡潔に組み合わせることができます。
  • 関数型が使える
  • 簡潔なラムダ式
  • KotlinのCollection API

これらの機能により、安全に簡潔で表現豊かな方法で関数型のコードを書くことが出来ます。したがって、純粋な関数(副作用なしの)をもっと簡単に作ることが出来ます。

式を使おう

// ダメ
fun getDefaultLocale(deliveryArea: String): Locale {
    val deliverAreaLower = deliveryArea.toLowerCase()
    if (deliverAreaLower == "germany" || deliverAreaLower == "austria") {
        return Locale.GERMAN
    }
    if (deliverAreaLower == "usa" || deliverAreaLower == "great britain") {
        return Locale.ENGLISH
    }
    if (deliverAreaLower == "france") {
        return Locale.FRENCH
    }
    return Locale.ENGLISH
}
// こうしよう
fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea.toLowerCase()) {
    "germany", "austria" -> Locale.GERMAN
    "usa", "great britain" -> Locale.ENGLISH
    "france" -> Locale.FRENCH
    else -> Locale.ENGLISH
}

考え方: if文を書いた時にもっと簡潔なwhenに書き換えができないかを考えてみよう。
try-catchも同様に便利な式です。

val json = """{"message":"HELLO"}"""
val message = try {
    JSONObject(json).getString("message")
} catch (ex: JSONException) {
    json
}

ユーティリティ関数のためのトップレベル(拡張)関数

Javaにおいて、しばしばUtilクラスにstaticなUtilメソッドを作ります。Kotlinで直接変換して書くと以下のようになります。

// ダメ
object StringUtil {
    fun countAmountOfX(string: String): Int{
        return string.length - string.replace("x", "").length
    }
}
StringUtil.countAmountOfX("xFunxWithxKotlinx")

KotlinはUtilクラスによる不要なラップを消して、代わりにトップレベル関数を使うことが出来ます。
また、時には拡張関数を追加して、可読性を高めることが出来ます。この方法はコードをもっと"語り口調"のようにすることができます。

// こうしよう
fun String.countAmountOfX(): Int {
    return length - replace("x", "").length
}
"xFunxWithxKotlinx".countAmountOfX()

Fluent Setterの代わりに名前付き引数を使おう

Javaのときに、fluent setter(Witherとも呼ばれる)は、引数に名前をつけるのを表現したり、デフォルトの引数をもったり、たくさんのパラメーターをもっと読みやすくしたり、エラーを起こしにくくするために使用されます。

//ダメ
val config = SearchConfig()
       .setRoot("~/folder")
       .setTerm("kotlin")
       .setRecursive(true)
       .setFollowSymlinks(true)

Kotlinでは、名前付きデフォルト引数は同じ役割を果たしますが、それらは言語に直接組み込まれています。

// こうしよう
val config2 = SearchConfig2(
       root = "~/folder",
       term = "kotlin",
       recursive = true,
       followSymlinks = true
)

apply()でオブジェクトの初期化処理をまとめよう

// ダメ
val dataSource = BasicDataSource()
dataSource.driverClassName = "com.mysql.jdbc.Driver"
dataSource.url = "jdbc:mysql://domain:3309/db"
dataSource.username = "username"
dataSource.password = "password"
dataSource.maxTotal = 40
dataSource.maxIdle = 40
dataSource.minIdle = 4

拡張関数のapply()は初期化のコードをまとめたり、集中させるのに役立ちます。それに加えて何度も変数名を繰り返す必要がありません。

// こうしよう
val dataSource = BasicDataSource().apply {
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://domain:3309/db"
    username = "username"
    password = "password"
    maxTotal = 40
    maxIdle = 40
    minIdle = 4
}

apply()はKotlinからJavaのライブラリを扱いたい時に時々便利です。

デフォルト引数を使ってオーバーロードをやめよう

デフォルト引数を実現するためにオーバーロードメソッドやオーバーロードコンストラクタを使わないでください。(いわゆる"method chaining" または "constructor chaining")

// ダメ
fun find(name: String){
    find(name, true)
}
fun find(name: String, recursive: Boolean){
}

それは冗長な表現です。この目的のために、Kotlinでは名前付き引数を利用することができます。

// こうしよう
fun find(name: String, recursive: Boolean = true){
}

実際にはオーバーロードはデフォルト引数を利用するために主に使われるため、メソッドとコンストラクタのオーバーロードのほぼすべてを削除できます。

Null可能性(Nullability)で簡潔に対処する

if-nullチェックを避けよう

Javaの方法でのNull可能性を扱うにはめんどうで、忘れやすいです。

// ダメ
if (order == null || order.customer == null || order.customer.address == null){
    throw IllegalArgumentException("Invalid Order")
}
val city = order.customer.address.city

毎回if-nullチェックを行う時、踏みとどまってください。Kotlinはもっとnullをハンドリングする良い方法を提供しています。しばしば?.によるnull-safe call、または?:によるエルビス演算子を使うことができます。

// こうしよう
val city = order?.customer?.address?.city ?: throw IllegalArgumentException("Invalid Order")

if-type チェックを避けよう

if-type チェックについても同様です。

// ダメ
if (service !is CustomerService) {
    throw IllegalArgumentException("No CustomerService")
}
service.getCustomer()

as??:を使うことで型チェックをすることができ、(smart)castがすることができ、また予想された型でないときに例外を投げられます。それが全て一つの文でできます!

// こうしよう
service as? CustomerService ?: throw IllegalArgumentException("No CustomerService")
service.getCustomer()

Not Nullアサーション!!を避けよう

// ダメ
order!!.customer!!.address!!.city

"二重の感嘆符はすこし失礼に見えるかもしれません。それはコンパイラに叫んでいるようです。こうなるのは意図的です。Kotlinの設計者はコンパイラが検証することができないアサーションを含まない、より良い解決策を行うように促しています。" Dmitry Jemerov と Svetlana IsakovaのKotlin in Actionより

let()を考えてみよう

ときどきlet()を使うことはifの置き換えとなります。しかし、読みにくくなる“train wrecks”を避けるために、健全な判断で利用する必要があります。それでもletを使うことを考えてください。

// ダメ
val order: Order? = findOrder()
if (order != null){
    dun(order.customer)
}

let()を使うことで、余分な変数が必要ありません。よって一文で行うことができました。

findOrder()?.let { dun(it.customer) }
// または
findOrder()?.customer?.let(::dun)

Value Object(値オブジェクト)を活かす

単一のプロパティを含むオブジェクトの場合でもデータクラスを使うと不変なValue Objectを簡単に書くことができます。だからもうValue Objectを使わないという言い訳はありません!

// ダメ
fun send(target: String){}

// こうしよう
fun send(target: EmailAddress){}
// 表現されていて、読みやすく、型安全

data class EmailAddress(val value: String)

Single expression functionによる簡潔なマッピング

// ダメ
fun mapToDTO(entity: SnippetEntity): SnippetDTO {
    val dto = SnippetDTO(
            code = entity.code,
            date = entity.date,
            author = "${entity.author.firstName} ${entity.author.lastName}"
    )
    return dto
}

Single expression functionと名前付き引数によりもっと簡単に、オブジェクト間のマッピングを簡潔に読みやすくかけます。

// こうしよう
fun mapToDTO(entity: SnippetEntity) = SnippetDTO(
        code = entity.code,
        date = entity.date,
        author = "${entity.author.firstName} ${entity.author.lastName}"
)
val dto = mapToDTO(entity)

もし拡張関数を好むのであれば、関数定義と関数の利用場所の両方を短く書くことができます。同時にValue Objectをマッピングロジックで汚染しません。

// こうしよう
fun SnippetEntity.toDTO() = SnippetDTO(
        code = code,
        date = date,
        author = "${author.firstName} ${author.lastName}"
)
val dto = entity.toDTO()

プロパティの初期化でコンストラクタのパラメータを参照しよう

プロパティを初期化するためだけにコンストラクタブロック(init ブロック)を書こうとしている場合二回考えてください。

// ダメ
class UsersClient(baseUrl: String, appName: String) {
    private val usersUrl: String
    private val httpClient: HttpClient
    init {
        usersUrl = "$baseUrl/users"
        val builder = HttpClientBuilder.create()
        builder.setUserAgent(appName)
        builder.setConnectionTimeToLive(10, TimeUnit.SECONDS)
        httpClient = builder.build()
    }
    fun getUsers(){
        //call service using httpClient and usersUrl
    }
}

プロパティの初期化(initブロックだけじゃなく)で主要なコンストラクタパラメータが利用できることを覚えておきましょう。
apply()は初期化処理をまとめて、一つの文で書くことを助けてくれます。

// こうしよう
class UsersClient(baseUrl: String, appName: String) {
    private val usersUrl = "$baseUrl/users"
    private val httpClient = HttpClientBuilder.create().apply {
        setUserAgent(appName)
        setConnectionTimeToLive(10, TimeUnit.SECONDS)
    }.build()
    fun getUsers(){
        //call service using httpClient and usersUrl
    }
}

状態を持たないInterfaceの実装のためのobject

Kotlinのobjectは状態を持たないフレームワークのインターフェースを実装する時に便利です。例えば、Vaadin 8のConverter interfaceの実装です。

// こうしよう
object StringToInstantConverter : Converter<String, Instant> {
    private val DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss Z")
            .withLocale(Locale.UK)
            .withZone(ZoneOffset.UTC)

    override fun convertToModel(value: String?, context: ValueContext?) = try {
        Result.ok(Instant.from(DATE_FORMATTER.parse(value)))
    } catch (ex: DateTimeParseException) {
        Result.error<Instant>(ex.message)
    }

    override fun convertToPresentation(value: Instant?, context: ValueContext?) =
            DATE_FORMATTER.format(value)
}

相乗効果の詳細についてはKotlin in Practice with Spring Boot and Vaadinにあります。

非構造化(Destructuring)

一方で、関数から複数の値が返ってきた時に非構造化は便利です。データクラスを定義するか(これは好ましい方法です)、Pairを使います(ペアには意味が含まれていないため表現力がありません)。

// こうしよう
data class ServiceConfig(val host: String, val port: Int)
fun createServiceConfig(): ServiceConfig {
    return ServiceConfig("api.domain.io", 9389)
}
//destructuring in action:
val (host, port) = createServiceConfig()

また、非構造化(Destructuring)を利用することで、mapから簡潔にkeyと値をなめることができます。

// こうしよう
val map = mapOf("api.domain.io" to 9389, "localhost" to 8080)
for ((host, port) in map){
    //...
}

その場限りの構造の構築

listOf,mapOf そして、はめこむ関数のtoはとても簡単に構造(JSONのような)の構築をするのに使われます。まだPythonやJavaScriptほどコンパクトではありませんが、Javaよりも優れています。

// こうしよう
val customer = mapOf(
        "name" to "Clair Grube",
        "age" to 30,
        "languages" to listOf("german", "english"),
        "address" to mapOf(
                "city" to "Leipzig",
                "street" to "Karl-Liebknecht-Straße 1",
                "zipCode" to "04107"
        )
)

しかし、通常はデータクラスとオブジェクトマッピングを利用して、JSONを作成する必要があります。しかし時には(例えばテストでは)非常に便利です。

ソースコード

idiomatic kotlinで実際に動くソースコードを見ることができます。