🤔

その Nullable で本当にいいの?

2023/12/28に公開

この記事は ミライトアドカレ 2023 の飛び込み記事です。

はじめに

勢いだけで書き殴ります。

本記事の 🤔 発言は保守するときの自分を含む誰かの苦しみです。
コードを書くときに数分いつもより考えるかどうかでこの先 5 年 10 年の苦しみが決まります。

いくつかのケースでそれを体験してみましょう。

0 と null に暗黙的に違う意味を持たせてはいけない

0 は値で null は空欄だ
同じように扱いたいなら、適切なのはどちらかだけのはず
暗黙的に意味が違うなら、きっちり使い分ける

ケース

$未納金: Int? = ...

この型定義から思いつくパターン

  • 🤔 0null は違うのだろうか
  • 🤔 0null の場合で同じように処理していいのだろうか
  • 🤔 コードは読めるけど意味がわからん、知ってる人教えて

確認しよう

  • 💡 未納金における 0null の違いを説明してみよう
  • 💡 0null に暗黙的な意味を設定してないか

たとえば

  • ✅ 違いがないなら Int にする

無駄に 惑わせてはいけない。

  • ✅ 違いがあるなら null にすべきが本当にそこか考える

たとえば「0 は未納金がないことを、null は支払いが発生していないことを表しています」と説明できたとする。
じゃあ その説明どおりコードにする んだよ。

class Payment {
    $未納金: Int
}

$支払い情報: Payment? = ...

「○△の状況なら null になりますね」は超頻出ケース。
それを全部安易に Int? にしてると地獄を見る。
まずとにかく バグる し、知ってる人が説明しないと進まないから 開発要員がスケールしない し、知ってる人は質問が集まって 忙しくなり続ける よ。

空文字を特別扱いしない

"" は値だから null の仲間ではなく "foo" の仲間だ
だから特別扱いしない

ケース

$コード: String = ...

String なのだが、コードを眺めると "" のケースがあるらしい。

if ($コード !== "") { ... }

この型定義と実装から思いつくパターン

  • 🤔 コードが "" とは
  • 🤔 String だが必須項目ではないのか
  • 🤔 それとも空文字のコードが存在するのか

確認しよう

  • 💡 "" に文字以外の意味を持たせていないか

たとえば

  • ✅ 未設定などを示すならちゃんと String? にする

"" は値だから null とごっちゃにしてはいけない。
必要ならちゃんと null を使う。

  • ✅ 不正入力を疑っているならきっちり終わらせておく

アプリなどから HTTP リクエストをとおして "" が入力される可能性はたしかにあるだろう。
ただそれを自分の懐に侵入させるな。
それは データやりとりの都合であってあなたのサービスの都合ではない

「このサービスのコードは任意項目なんです」であれば、そのとおりコードにする んだよ。

$入力: String = ...
$コード: String? = if ($入力 == "") { null } else { $入力 }

String? なら「任意項目感」が得られる。

「あ、それ String だけど空文字チェック絶対してね」なんて言われたらどう思うか。
「知らんわ」 しかない。

ちなみに言われなかったら そのままバグ ですおめでとうございます。

そして 1 つ String を疑うことになったプロジェクトは、ほか数百ある すべての String が信用できなくなる

どう楽観的に想像したって地獄。

ところで

  • ❌ DB のカラムが not null なので String? にできません

わかる、わかるよ、とてもわかる。
DB で null が使えないから String にして空文字を特別扱いしている。
とてもよくわかる、けど 心底そんなことは関係ない んだ。

DB で null が使えないなら、テーブルに書くときに null -> "" の変換をして、テーブルから読むときに "" -> null の変換をすればいいだけだ。

HTTP や DB の都合とあなたのサービスの都合を混同してはいけない
ここが崩れるとコードがカオスになるのは一瞬だ。

さらに

たぶん $コード には「文字数」とか「文字種」とかの制約がたくさんある。
String ではそれが伝わらない。

Code クラスを作るともっとよくなる。

class Code {
    $value: String

    コンストラクタ($value: String) {
        // new のための絶対条件
        assert(3 文字だよね)
        assert(アルファベットだけだよね)
        
        $value = $value
    }
}

$コード: Code? = ...

new ができたら完全に信頼できるようにする のだ。

この実装から思いつくパターン

空文字の可能性が完全に消えた ね。

リストを Nullable にしない

そもそも複数を扱えるデータ構造で空と無を区別する必要があるのか

ケース

$オプションリスト: List<String>? = ...
  • 🤔 []null は違うのか

確認しよう

  • 💡 []null の違いを説明してみよう
  • 💡 []null に暗黙的な意味を設定してないか

たとえば

  • ✅ 違いがあるなら null にすべきが本当にそこか考える

たとえば「[] は対象ユーザがオプション未契約なことを、null は対象ユーザが存在しないことを表しています」と説明できたとする。
後者のケースで処理を続ける必要は本当にあるのか。
それただのエラーなのでは

こうではなく

こうしよう

List<String>? という変数に受け、さも「このあと普通に使うかも」というそぶりを見せない。
いつかエラーにするなら今すぐにエラーにするんだ

  • ✅ データベースから読み取る処理が null っていうので

さっきも紹介したとおり 部外者がなんと言おうが関係ない、外の都合を持ち込むべからず

- $オプションリスト: List<String>? = common.getUser(id).options
+ $オプションリスト: List<String> = common.getUser(id).options else []

ところで

これ以外に「深く考えてなかった、find 処理が空振りしたら null だろって思ってた」だけのケースが実は一番多いのではないかとも思う。
個人的な仮説のひとつでしかないが、「なかったを特別扱いする」発想で後続のコードを考えているのではないかと思う。

$オプションリスト: List<Option>? = ...
$請求対象リスト:List<Int> = []

if ($オプションリスト != null) {
    for ($オプションリスト!! -> $オプション) {
        $請求対象リスト.append($オプション.price)
    }
}

return $請求対象リスト

複数を扱えるデータ構造は 大体空っぽでも問題なく動くようになっている

$オプションリスト: List<Option> = ...
$請求対象リスト: List<Int> = $オプションリスト.map { $オプション.price }

return $請求対象リスト

中身の数を気にしたら負け みたいな感覚にシフトすると見え方が変わるのではないかと思う。

ついでに

PHP の @return array|null について

PHP の array を使っていると少し感覚が鈍るかもしれないなって最近思った。
なんかこいつ配列だったりマップだったりするし。

こういう ↓ ケースで @returnarray|null なのは妥当かもしれない。
( no hit 例外論は一旦棚上げ )

// @return array|null
function findUser($id) { ... }

$user = findUser($id)
var_dump($user)
// [id => 1, name => John, age => 29] もしくは null

$user は一人分の情報なのでこの array本質的には単数系
だから |null をしないと no hit を表現できないのはまだわかる。

でもこういう ↓ ケースでは array|null ではなく array が妥当。

// @return array
function findUsers($status) { ... }

$users = findUsers($status)
var_dump($users)
// [[id => 1, name => John, age => 29], [id => 2, name => ...]] もしくは []

この array複数形 なので @return array[] が返ればことたりる。

PHP の array を使うときは 単一要素の複数プロパティなのか複数要素なのか を意識するとちょっとイイかも。

関連する要素の null をまとめる

? から得られる情報を可能な限り増やす

ケース

class Application {
    $使用者: String
    $利用開始: Date?
    $利用終了: Date?
    $更新方法: String?
}

あるクラスのプロパティに ? が 3 つある。
( 買い切り契約とサブスク契約みたいななんかそんなやつ )

このコードを見たときの印象は 「やべぇの引き当てちゃったな、状況がなんもわからん」 というところ。

この型定義から得られる null が含まれるケースの列挙

利用開始 利用終了 更新方法
o o o
o o -
o - o
o - -
- o o
- o -
- - o
- - -

? が 3 つあるので当然 null になったりならなかったりは全部で 8 パターンになる。

  • 🤔 $利用開始 が指定された場合に $利用終了 って null にできたっけ?
  • 🤔 $更新方法 だけ null ってケースある?

確認しよう

  • 💡 申し込みについて説明しよう
  • 💡 連動関係や排他関係にある null を整理しよう

たとえば

  • ✅ 連動する要素はまとめる

たとえば「$利用開始 したら $利用終了$更新方法 は必須です」と説明できたとする。
じゃあ その説明どおりコードにする んだよ。

class Subscription {
    $利用開始: Date
    $利用終了: Date
    $更新方法: String
}
class Application {
    $使用者: String
    $サブスク: Subscription?
}

この型定義から得られる null が含まれるケースの列挙

利用開始 利用終了 更新方法
o o o
- - -

このコードを見たときの印象は 「地味だけど超いい仕事をする人が過去にいたんだな」 というところ。

プロジェクトには変数がいくつある?
1000 か? 5000 か?
1 つの変数について確認するのに聞く人と聞かれる人で何分拘束される?
10 分か? 30 分か?
「読めばわかる」と「聞かないとわからない」のコストの差はどれくらいだ?

? をちょっと真剣に考えるだけで 4 倍の情報密度 だ。
いつまでも開発速度を保ちたいならいつもよりちょっとだけ丁寧に考えよう。

おわりに

自分で書いたプロジェクト内すべての ? について説明できるようになりましょう。

改めてになりますが、この記事で伝えたいのはテクや具体的な対応方法ではありません。
扱っている要素と真剣に向き合いましょう というはなしです。

扱っている要素に向き合わなければ、どんな設計論も言語・ライブラリ選定も活かせません。

地味なところですが、がんばり続けたいですね。( 自戒込 )

Discussion