Nullableフィールドの使い方

これは30日チャレンジの1日目(2019/09/09)に書かれた文章です

Nullableという単語を聞いてどのような印象を持つだろうか。 筆者は「選択肢を減らし、意思決定を遅延させる機能」のような印象を持っていた。

Nullableという単語事態ははAPIやデータベーススキーマ定義の際に用いられることが多い。 例えば関数やAPI、あるいはDatabaseスキーマの定義において、特定のフィールド(カラム)の型に対してNullableな制約を利用する場面がある。

恥ずかしながら筆者自信の経験において、とくにまだドメイン知識が十分に蓄積されていないような状況で「ひとまずNullableを指定しておく」といった意思決定をしたこともある*1。 しかし「ひとまずNullable」とする判断は、果たして意思決定の遅延という目的において本当に正しい選択なのだろうか。

本文を書くにあたり、Nullableフィールドの使い方に関する記事を参考にさせていただいた。

上記の記事ではアプリケーションの「進化性」の観点からNullableをいかに扱うべきかという問いに対するひとつの解を示している。 以下では進化性に着目してNullabilityをどう使えばよいか議論したい。

進化性とは、対象物の経時的な変化に関する性質を述べている。 「進化性の観点からデータベーススキーマを考える」というとき、例えば「当初はNullableに設定していたカラムが非Nullableに変化する」ことや、またはその逆の状況において発生しうる問題に関心をもつ。

さて、それでは実際に「当初はNullableに設定していたカラムが非Nullableに変化する」状況を考えてみる。

結論から述べると、じつはこの状況では「ひとまずNullable」に設定する意思決定はうまく働く。 なぜならNullableなフィールドを持つデータは出力時の処理において前方互換性を持つからだ。 すなわち、Nullableなフィールドを前提として書かれたアプリケーションコードは値のNull検証を含んでいるはずで、当然ながらそのコードは非Nullableに変わったあとも正しく動作する。

では、関数のある引数に対して「ひとまずNullable(optional)」を設定した場合をどうだろうか。 じつはこの場合(すなわちNullableな引数を非Nullableにする操作)は破壊的な変更となる。 理由は先ほどの真逆であり、Nullableな入力は前方互換性を持たないからである。 つまり、関数がNullを受け付けなくなるためにクライアンド(関数を使う)側でNull検証が要求される。

さらに議論を進めて、「非NullableがNullableに変化する場合」を考えてみるとこちらも先ほどの逆の結果を得る。 すなわち、入力処理は互換性をもち「ひとまずNullable」がうまく働く一方で、出力処理は破壊的変更となるのである。

以上の議論から、「ひとまずNullable」という意思決定は少なくとも万能でないことは明らかである。 意思決定を遅延し選択肢を残しているように見えても、実際には選択肢を減らす(=互換性のない)選択を行っているのである。

本文では進化性の観点から、Nullable制約と互換性の関係を見てきた。 しかしながら、進化性とは別の視点から考えてみると、また異なる解が得られる可能性がある。

例えば「Nullableと空文字列をいかに使い分けるか」を考えるとき、進化性の観点はあまり大きな意味をもたない。 むしろ、フィールドの値に対する意味付けといった観点から考えてみることが必要ではないだろうか。

後の記事において、引き続きNullableに関する議論をつづけていく。


これは30日チャレンジの2日目(2019/09/10)に書かれた文章です

昨日は、進化性の観点から、Nullable制約と互換性の関係を見てきた。
本文では「Nullableと空文字列をいかに使い分けるか」に関して考えたい。

データ型をもつシステム(プログラミング言語やGraphQLをはじめとるWebAPI)において、特定の値に対して特別な意味をもたせることがある。 例えば、Unix環境におけるプロセスは処理終了時に数値型の0を返すことで正常終了をOSに伝える。 あるいは、位置や長さといった概念を表す変数・フィールドは定義からして非負の値をもつことが期待される。

ここで冒頭の問い「Nullableと空文字列をいかに使い分けるか」に戻りたい。 例えば、指名を表す変数nameがNullable指定されている場合、変数値がNullの場合と空文字列の場合は区別すべきだろうか。

結論から先に述べると、この議論に正解はないと筆者は考えている。 むしろ、開発の主体者の間で「どのようなポリシーを設定するか」が答えるべき正しい問いだ。

Nullableとゼロ値の扱いに関するポリシーを定めないことは、変数値の解釈の多義性を許容することを意味する。 そして解釈の多義性を許す以上、認識齟齬を防ぐために開発者間でコミュニケーションが要求されたり、あるいは齟齬があっても問題なく動作するように両方を状況を処理できるような実装が必要になる。

Null値とゼロ値との使い分けに関するポリシーは可能な限りプロジェクトの初期に設けることが望ましい。 昨日の議論で見たとおり、Nullable制約を課す(外す)操作はデータの入出力において互換性を壊す可能性があるからだ。 開発が進んでそこかしこにNullableが存在している状況では、新たにポリシーを設定するには互換性を破壊しないための検証に非常に大きな労力を要する。

じつは筆者が現在取り組むプロジェクトでは、これまでポリシーを設けずに開発を進めてきた。 そして先日、新しく追加されるエンドポイントのレスポンスフィールド定義においてチーム内で議論を設けた。 議論の末、「選択肢を狭めない」ことを理由にNullableなフィールドを追加することに至ったのだが、我々はNull値とゼロ値の意味付けに関するポリシーを持っていなかったため、それらの値に対して恣意的に意味付けを行ってしまった。

幸いなことにクライアントサイドではNull値もゼロ値も丸め込んで扱えるため、当面の間は問題にならないだろう。 とはいえ、フレームワークを変更したり、開発者が代わったり、その他さまざまな要因で問題は発生しうる。 つまり、Null値とゼロ値にたいする認識の齟齬が、原因の特定が難しい類のバグとなって表出するのだ。 もしポリシーが設定されていてば、認識の齟齬を防ぎ、不必要な問題に悩まなくて済むだろう。

API定義のように開発主体者の境界になりやすい部分では、まずはじめにNullableフィールドに関するポリシーを定めることを忘れないようにされたい。

*1:念のために補足しておくと、職業としてエンジニアを名乗る以前のお話だ