その Nullable で本当にいいの?
この記事は ミライトアドカレ 2023 の飛び込み記事です。
はじめに
勢いだけで書き殴ります。
本記事の 🤔 発言は保守するときの自分を含む誰かの苦しみです。
コードを書くときに数分いつもより考えるかどうかでこの先 5 年 10 年の苦しみが決まります。
いくつかのケースでそれを体験してみましょう。
0 と null に暗黙的に違う意味を持たせてはいけない
0
は値で null
は空欄だ
同じように扱いたいなら、適切なのはどちらかだけのはず
暗黙的に意味が違うなら、きっちり使い分ける
ケース
$未納金: Int? = ...
この型定義から思いつくパターン
- 🤔
0
とnull
は違うのだろうか - 🤔
0
とnull
の場合で同じように処理していいのだろうか - 🤔 コードは読めるけど意味がわからん、知ってる人教えて
確認しよう
- 💡 未納金における
0
とnull
の違いを説明してみよう - 💡
0
とnull
に暗黙的な意味を設定してないか
たとえば
- ✅ 違いがないなら
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
を使っていると少し感覚が鈍るかもしれないなって最近思った。
なんかこいつ配列だったりマップだったりするし。
こういう ↓ ケースで @return
が array|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