可読性の高いコードは、変数の状態が直交である
可読性の高いコードは、変数の状態が直交である
はじめに
「読みやすいコードのガイドライン -持続可能なソフトウェア開発のために」の内容 4 章のアウトプットとして記事を書いています。
また、TypeScript にて例を記載しています。
なぜ変数の「状態」が可読性の高いコードに寄与するのか
可読性の高いコードの要件
そもそも可読性の高いコードの要件は一般的に以下が満たされていることです。
- 単純であること
- 意図が明確であること
- 独立性が高いこと
- 構造化されていること
例えば「意図が明確であること」についてです。
変数名がflag
であったときに、この変数が何をしているのかわかりません。
isVisible
であったらどうでしょう?変数名だけで「なにか表示可能なものがあって、true になったことで表示非表示が切り替わる」ことがわかると思います。
変数の「状態」が可読性向上につながる要因
変数の「状態」について考えるメリットとして、以下の点で可読性向上につながることが挙げられます。
- 変数を持つ状態を単純にすることで、可読性の高いコードの要件である「単純であること」が実現できる
- 状態が限定された変数を使うことで「独立性が高い」状態になり「構造化されている」ことを担保できる
これからどんなことを考えれば、変数の「状態」が良いコードに寄与するかを書いていきたいと思います。
タイトル名にある通り、関数同士を「直交」にすることで、可読性の高いコードへの一歩になります。
複数の変数間の関係
ベストプラクティス
変数同士に関係がなくなるように、直交した状態の関数で関数定義を行うことです。(=直交な状態にする)
非直交な関係があった場合は「関数による置き換え」と「直和型で置き換え」を行う。
「直交」という概念
直交する関数と直交していない関数の例を見てみましょう。
直交な例
function getPerson(
name: string,
age: number
): {
name: string;
age: number;
} {
return {
name,
age,
};
}
name
とage
はお互い関係を持たない関数なので直交といえます
非直交な例 ①
次に極端な例ですが、名前を表示する関数があったとしましょう。
function displayUserName(
firstName: string,
lastName: string,
fullName: string
): void {
console.log("姓", lastName);
console.log("名", firstName);
console.log("名前", fullName);
}
fullName
は、基本的にfirstName
とlastName
を組み合わせた関数となるでしょう。
しかし、この関数ではdisplayUserName("太郎","山田","中田花子")
という値をもってしまい、バグの温床になってしまいます。(本来ならば、fullName
はfirstName
とlastName
を組み合わせた文字になるべき)
非直交な例 ②
もう一つ、非直交な例をあげます。
以下の型は、成功時はstatus
が非 null になり、失敗時にはerrorType
が非 null になってしまいます。
type StatusResponse = {
// 取得に失敗した場合、nullになる
status: Status | null;
// 取得に失敗した場合の理由をを示す(成功した場合はnullになる)
errorType: ErrorType | null;
};
このstatus
とerrorType
についても、不正な組み合わせがあります。
もしstatus
とerrorType
の両方が非 null になったり、逆に両方とも null になる場合は問い合わせが成功したとも失敗したとも言えなくなってしまいます。これは、お互いの変数に関係が生まれているので、非直交な状態とみなせます。
「直交」にする方法
上記に挙げた「非直交な例 ①」、「非直交な例 ②」を可読性の高いコードに直してみましょう。
変数を関数として置き換える
複数変数持っている変数を関数に置き換えることで、「非直交な例 ①」を解消することができます。
その際、置き換えできるかは、従属の関係になっているかを考える必要があります。
例えば、「非直交な例 ①」の場合はfirstName
とlastName
が決まれば、fullName
が決まります。
このように一方の変数 A が確定することによって、も一法の変数 B の値が確定する場合、「B は A に従属している」と定義できます。変数 B が変数 A に従属しているならば、A を使った関数で B を置き換えることができます。
今回の場合は、fullName
をgetFullName
などの関数に置き替えると、従属関係の関数を引数から削除ができ、不正な値になることがありません。
function displayUserName(firstName: string, lastName: string): void {
console.log("姓", lastName);
console.log("名", firstName);
console.log("名前", getFullName(firstName, lastName));
}
function getFullName(firstName: string, lastName: string): string {
return lastName + firstName;
}
上例のように関数に切り出すと、従属関係にある変数が減ることで関数がシンプルになりますね。
直和型で置き換える
2 つの値が従属の関係にない時は、値を関数に置き換える方法は使えません。
例えば、「非直交な例 ②」において、status
が null であった場合、失敗したことになりerrorType
が非 null であることが決まります。
しかし、どんな値が入るのかまではわかりません。
また、errorType
が null だった場合は、status
が非 null であることはわかりますが、こちらもどんな値が入るかまではわからないです。
このような直交でも従属でもない関係に関しては、直和側を使うとよいでしょう。
TypeScript で直和側を表現するには、判別可能なユニオン型(discriminated union)を使いましょう。
「非直交な例 ②」は以下のように書き換えます。
type StatusResponse = Success | Failure;
type Success = { type: "Success"; status: Status; errorType: null };
type Failure = { type: "Failure"; status: null; errorType: ErrorType };
変えた点としては、以下の 3 点を行いました。
-
Success
とFailure
の型を定義する -
Success
とFailure
とそれぞれに、type というディスクリミネータを追加する -
Success
とFailure
で null になるプロパティを指定している
上記 3 つをすることで、コンパイラーが型の絞り込みを理解でき、また、status
とerrorType
のどちらが一方が null であり、もう片方が非 null であることを保証できるようになります。
Discussion