🫖

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

2024/12/25に公開

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

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

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

これを知ってから自分が書くコードの可読性が一気に上がったと思います。

是非活用していきましょう!

脚注
  1. https://detekt.dev/docs/rules/complexity/#cognitivecomplexmethod ↩︎

  2. プログラミングの概念っぽく言えば、"表明"がはっきりした。 https://ja.wikipedia.org/wiki/表明 ↩︎

Discussion