空の箱

からのはこ

Kotlinでrequireを使って引数とかのAssertionをいい感じに表現する

こういうコードを書いた覚えはないだろうか

例えば商品の代金を計算したいとする。その時の計算処理は一般化し、以下のように記述することができる。

fun calcPurchasePrice(price: Int, quantity: Int): Int {
    if (price < 0 || quantity < 0) {
        throw IllegalArgumentException("Price and quantity must be positive")
    }
    return price * quantity
}

ですが、このコードは少し回りくどい。なぜかというと、ifに続く実装を見なければ、「この判定が何者なのか?」がわからないから。

そもそもdetektCognitiveComplexMethod*1にも見られるように、Kotlinの思想としてifを多用すること自体がアンチパターンに入っている。

Kotlinを使ってこのコードを表現するのであればもう少し宣言的に書ける。しかし、このコードをよりスマートにリファクタするにはどうすればいいだろうか?

そのコード、require で書けるよ

早速答えになってしまうが、上記のような場合にはrequireを使うのが好ましいと考えている。

requireを使うことで以下のように書き換えることができる。

fun calcPurchasePrice(price: Int, quantity: Int): Int {
    require(price >= 0 && quantity >= 0){
        "Price and quantity must be positive"
    }
    return price * quantity
}

requireは条件がfalseになった場合IllegalArgumentExceptionを吐いてくれる。

このコードの良いところはifがなくなったこともそうだが、もともとのifで判定しようとしていたことが一目でわかるようになったこと*2

リファクタするまではifの実装を見るまで「引数の判定なのか、それともまた別のなにかの処理なのか?」というのはifの2文字を見ただけではわからない。

しかし、requireはそれだけで引数の判定をしているんだなということを雄弁に語ってくれる。

余談だが、非NULL判定をしなければならない場合はrequireNotNullを使うことができる。

引数以外をAssertionするときは?

ここまでの話で、引数に関してはrequireを使ったAssertionを噛ませることでIllegalArgumentExceptionを吐いてくれるようになった。

では引数以外の値をAssertionする場合はどうすればいいだろうか?

例えば以下のように、"環境変数でセール値引きを適用するかをコントロールしている"というシチュエーションを考える。

fun calcPurchaseSalePrice(price: Double, quantity: Int): Double {
    require(price >= 0 && quantity >= 0){
        "Price and quantity must be positive"
    }
    val saleFlg = System.getenv()["SALE_FLG"]
    //? saleFlgがnullかどうかの判定を書かないとダメ?
    return price * quantity * 0.5
}

またもいきなり答えだが、ここではcheckNotNullという関数を使うことができる。

fun calcPurchaseSalePrice(price: Double, quantity: Int): Double {
    require(price >= 0 && quantity >= 0){
        "Price and quantity must be positive"
    }
    val saleFlg = System.getenv()["SALE_FLG"]
    checkNotNull(saleFlg){
        "Not found SALE_FLG"
    }

    return price * quantity * 0.5
}

checkNotNullのAssertionに引っかかった場合は、IllegalStateExceptionを吐いてくれる。

おわり

Kotlinの4つの哲学の一つ・簡潔性を語る上で象徴的なrequire, checkを紹介した。

表にまとめるとこんな感じ。

チェック対象 使う関数 Assertionエラー時の例外
引数 require (requireNotNull) IllegalArgumentException
引数以外 check (checkNotNull) IllegalStateException

バックエンドでは入力値をバリデーションする機会が必ずある。その場合必然的にifを多用する必要があるが、それをrequireに置き換えることができる。これを知ってから自分が書くコードの可読性が一気に上がったと思う。是非活用したいきたい。

*1:https://detekt.dev/docs/rules/complexity/##cognitivecomplexmethod

*2:プログラミングの概念っぽく言えば、"表明"がはっきりした。 https://ja.wikipedia.org/wiki/%E8%A1%A8%E6%98%8E