LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック:第47回 不履行権の不履行

こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。

この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 47 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)

不履行権の不履行

User を意味するデータモデルが以下のように定義されているとします。

data class UserModel(
    val id: Int,
    val name: String,
    val profileImageUri: Uri?,
    val birthDate: Date?,
    ...
)

ここで、「無効なユーザ」を表現するために UserModel(0, "", null, null, ...) という Null Object を作る必要があると仮定しましょう。(本来、Null Object を作るかどうかは慎重に検討するべきですが、ここではプロダクトの歴史的経緯により作らざるを得ないとします。当然ながら、「id が 0 なら無効なユーザ」といった定義は可能ならば避けるべきです。Null Object については 火の null 所に煙は立た null を参照してください。)

さらに、Null Object を簡単に作成できるように、以下のようにデフォルトパラメータを定義しました。

data class UserModel(
    val id: Int = 0, // 0 represents an invalid user
    val name: String = "",
    val profileImageUri: Uri? = null,
    val birthDate: Date? = null,
    ...
)

Kotlin に限らず、様々な言語でデフォルトパラメータを使うことができ、実引数を省略するとデフォルトパラメータが使用されます。このデフォルトパラメータによって、UserModel の Null Object は以下のように作成できます。

val invalidUserModel = UserModel()

val isInvalid = userModel == invalidUserModel

この UserModel の定義になにか問題はありますか?

履行できるものは履行する

基本的には、Null Object を作るためにデフォルトパラメータを使うのは避けたほうがよい です。今回のケースでは、以下のような UserModel のインスタンスが誤って作成されてしまう可能性があります。

val user = UserModel(
    name = "John Doe",
    profileImageUri = imageUrl,
    birthDate = date,
    ...
)

このように、id の指定を忘れてしまうと、そのユーザの id は「無効なもの」とされてしまいます。一方で、user == UserModel() の結果は false になるため、バグの原因になりかねません。このような事態を招く原因は、デフォルトパラメータの値 id = 0 は、 一般に広く使われる値ではなく、特殊な意味を示す値である点にあります。

もし、Null Object が必要なのであるならば、デフォルトパラメータを使わず、以下のようにインスタンスを作成するべきです。

data class UserModel(
    val id: Int,
    val name: String,
    val profileImageUri: Uri?,
    val birthDate: Date?,
    ...
) {
    companion object {
        val INVALID = UserModel(0, "", null, null)
    }
}

本題とは外れますが、Null Object を直和型で表現する方法もあります。これは特に、Invalid を通常の UserModel から区別する必要があるときに有効です。

sealed interface UserModel {
    data class Valid(
        val id: Int,
        val name: String,
        ...
    ) : UserModel

    data object Invalid : UserModel
}

また、そもそも Null Object を使う必要がないならば、型安全な null の値を使うのも選択肢に入ります。

val userModel: UserModel? = ... // null represents an invalid user.

いずれにせよ、デフォルトパラメータの値は、広く使われる一般的な値であるべきで、特殊用途の値を指定すべきではありません。 デフォルトパラメータを使う場合は、「値を与えない」ことが一般的な状況かと、「値を与えなかった場合に何が起きるか」が容易に想像つくかを確認してください。

一言まとめ

デフォルトパラメータに使う値は、共通して使われる「普通の」値であるべきで、特殊な値は使うべきでない。

キーワード: default parameter, null object, special value

コード品質向上のテクニックの他の記事を読む

コード品質向上のテクニックの記事一覧