「読みやすい」とはどういうことか? コード品質の一指標

12 min読了の目安(約7700字IDEAアイデア記事
Likes132

「良いコード」とは何でしょうか? コードの品質には色々な指標がありますが、「読みやすいコードは良いコードである」というのは一つの指標として多くの方が認めるところではないでしょうか。しかし、では読みやすいコードとはどのようなコードかというのもなかなか難しい問題です。

この記事では、品質の良いコードとしての「読みやすいコード」に対する筆者の考え方を共有します。もちろんこれが唯一解だと主張するつもりはありませんが、参考になった・共感したという方はぜひこの記事を周りに教えてあげてください。

なお、サンプルコードはTypeScriptを使って示しますが、必要に応じて説明するのでTypeScriptの経験が無い方でも読むことができます。

短いまとめ

  • 読みやすいコードとは、書き手の意図が伝わりやすいコードです。
  • 書き手の意図を読み手に伝えるには、読み手に意図を推論してもらうためのヒントを残します。
  • 複数の書き方があるときは、もっともヒントになりやすい(言い換えれば、情報量が多い)選択肢を選びましょう。
  • ヒントを与えるときは合理的な読者を優遇すべきです。

以下からは、いくつかの具体例を通じてこの考え方を解説していきます。

letconstの問題

JavaScript/TypeScriptでは、変数を宣言する際にvarを使う・letを使う・constを使うという3つの選択肢があります。varはJavaScriptの最初から存在しており、letconstはES2015で追加されました。varと残りの2つの間には、スコープが関数スコープかブロックスコープかという違いや、同じ変数の再宣言が可能かどうかといった違いがあります。letconstの方が後から追加されただけあって優れているので、varは使わずにこの2つのどちらかを使えば問題ありません。

letconstの違いは、後者(const)で宣言された変数は再代入ができないという点にあります。

let foo = 123;
foo = 456; // これはOK

const bar = 0;
bar = 999; // これはエラー

JavaScript/TypeScriptにおいて、変数を宣言するたびに変数をlet/constのどちらで宣言するかという選択肢が発生します。変数に再代入される場合はletのみが選択肢となりますから、必然的にletが使われることになります。しかし、実は再代入されない(constで宣言してもよい)変数というのはJavaScript/TypeScriptプログラミングにおいてかなり多く発生します。どんなプログラムかにも依りますが、ほとんどのケースでは再代入されない変数が9割以上といっても過言ではないでしょう。その場合にletconstのどちらを使えばいいのかというのが問題です。

読み手にヒントを与えるという観点からは、再代入されない変数にはconstを使うべきです。なぜなら、constで宣言された変数には「この変数には再代入されない」という情報が載っているのに対して、letでは「この変数は再代入されるかもしれないしされないかもしれない」ということになり、情報が無いからです。constの方が読み手に多くの情報が与えられるので、constを使える変数に対しては必然的にconstを選択することになります。

実際、変数への再代入は「その変数に入っているものが状況によって異なる」という複雑性をロジックに与えます。そのため、プログラムを読解する際には変数の再代入には特に注意する必要があります。letで宣言された「再代入されるかもしれない変数」は、constに比べるとプログラムを読む人の負担が大きくなります。そのため、letを使用するのは必要最小限にすべきです。

また、合理的に考える読み手なら、上の議論を前提として「わざわざconstではなくletを使うということはこの変数はあとで再代入されるに違いない」と思うでしょう。もし再代入されない変数にletを使うと、この読み手に誤った理解を与え混乱させてしまうことになります。特にこの記事の考え方に従うならば、プログラムとは合理性・必然性の塊であり、合理的な読解を裏切るのはそれ自体が非合理的なことですから、合理的な読み手は最大限優遇すべきです。

なお、letconstの話題に関しては、「もともとconstは定数のみに使用することを意図されており、普通の変数は全部letで宣言されることが想定されていた」という言説があります。しかし、だからといってこれに律儀に従う必要はありません。歴史を重んじるのは大事なことですが、constのほうが情報量が増えるという現実的なメリットに対しては釣り合わないからです。

readonlyを使う

似たような話題として、TypeScriptはreadonly配列やreadonlyプロパティといった機能があります。例えば、number[]が「数値の配列型」であるのに対して、reaadonly number[]は「書き換え不可能な数値の配列型」を表します。

このreadonlyは関数の引数で使うと特に威力を発揮します。例えば、与えられた数値の配列の和を返すsum関数を考えてみましょう。

function sum(arr: number[]): number {
    let result = 0;
    for (const num of arr) {
        result += num;
    }
    return result;
}

この関数は与えられた配列arrを読み取るだけで書き換えないので、次のようにreadonlyを使うことができます。こうすると、sumの中でarrを書き換えることはできなくなります(コンパイルエラーとなります)。

function sum(arr: readonly number[]): number {
    let result = 0;
    for (const num of arr) {
        result += num;
    }
    return result;
}

筆者の考えでは、sumの引数はこのようにreadonly number[]型にすべきです。なぜなら、やはりreadonlyと書いてある方が情報が増えるからです。プログラムの読み手は、関数の型(引数の型)を見ただけで「この関数は与えられた引数の配列を書き換えない関数である」ということが分かります。一方で、readonlyと書いていない関数は与えられた配列(やオブジェクト)を書き換えるのか書き換えないのか分からないし、それどころか「readonlyと書いていないと言うことは書き換える関数だ」と考えるほうが合理的ですらあります。

惜しむらくは、readonlyが長くてそこかしこに書くのが面倒くさいということです。筆者の考えでは多少の面倒くささよりもプログラムの読みやすさのほうが優先されますが、ネストしたオブジェクトなどになるとさすがに面倒くさすぎると思わないでもありません。その点、書き換え不可をデフォルトとしたRustなどはデザインが上手ですね。

正確なインターフェースを付ける

型のある言語では[1]、引数の型や返り値の型によって関数のインターフェースを定義します。このインターフェースは、正確にすればするほど情報量が増えます。よって、なるべく正確なインターフェースを記述するべきです。

一つややTypeScript的な例を出しておきます。与えられた引数の文字列によって異なる数値を返す関数を作ってみましょう。

function getFontSize(size: string): number {
    if (size === "small") {
        return 10;
    } else if (size === "medium") {
        return 16;
    } else  {
        return 24;
    }
}

このgetFontSize関数は、"small"を渡されたら10を、"medium"を渡されたら16を、それ以外の文字列を渡されたら24を返します。そうはいっても、この「それ以外の文字列」というのが微妙ですね。具体的には何でしょうか? "hogehoge"とか"ピカチュウ"とかを渡せばいいのでしょうか? 恐らく、そういった利用は想定されていないでしょう。

TypeScriptでは、リテラル型とユニオン型の組み合わせによって「文字列(string)」よりもより具体的な型指定をすることができます。

type SizeString = "small" | "medium" | "large";

function getFontSize(size: SizeString): number {
    if (size === "small") {
        return 10;
    } else if (size === "medium") {
        return 16;
    } else  {
        return 24;
    }
}

上の例ではSizeString型は「"small"または"medium"または"large"という文字列の型」として定義されています。よって、SizeString型を引数に取るgetFontSize関数はこの3種類の文字列しか受け取ることができません。こうすれば、24が欲しいときは"large"を渡せばいい(それ以外は渡せない)ことが明らかになりました。

関数のインターフェースがstringからSizeStringというより厳しいものになりました。一般に、より厳しいインターフェースのほうが情報量の多いインターフェースです。

リテラル型・ユニオン型というのはTypeScript以外の言語では珍しい概念ですが、いわゆるenumを使うと他の言語でも似たような表現ができる場面が多いでしょう(特にRustのようにADTを持っている言語は優秀ですね)。TypeScriptにもenumはありますが古い機能なので使うべきではなく、上述のようなリテラル型・ユニオン型のほうが適しています。

ちなみに、やろうと思えばgetFontSizeの返り値の型を10 | 16 | 24というユニオン型にすることもできるのですが、ここではnumberと書かれています。これにも意味があり、「10、16、24といった具体的な数値には意味がない」ということを読み手に伝える役割を果たしています。この関数は何らかの数値を返す関数なのであり、個々の数値に依存すべきではないということを関数のインターフェースを通じて示しているのです。

publicとprivate

クラスを作ってフィールドを作るときは、そのフィールドをpublic(クラス外からも見える)にするかprivate(クラス内にのみ見える)にするかという選択肢が発生します。フィールドをpublicにするかprivateにするかによって、読み手に与える情報は大きく異なります。

次のHumanクラスでは、nameagepresentCountがpublicなフィールドです。

class Human {
  name: string;
  age: number;
  presentCount: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.presentCount = 0;
  }

  grow() {
    this.age++;
    this.presentCount++;
  }

  getPresent() {
    if (this.presentCount <= 0) {
      throw new Error("You have no present!");
    }
    this.presentCount--;
    return "present";
  }
}

このHumanは「1回grow()するごとに1回getPresent()する権利を得る」というロジックを持っています。それはpresentCountによって実現されており、最初presentCountは0ですが、1回grow()するごとにpresentCountが溜まって、それはgetPresent()で使うことができます。

結論から言ってしまえば、このpresentCountはprivateのほうがいいですよね。上のコードは設計など色々な観点から考察できそうですが、この記事ではやはり「読みやすさ」の観点から考えていきます。

TypeScriptでは、クラスのフィールドをpublicにするかprivateにするか[2]、そしてreadonlyにするか否かという選択肢があります。今回、presentCountやその他のフィールドがpublicである(しかもreadonlyではない)ので、クラスの外部から書き換えられる余地があります。むしろ、privateもreadonlyも使わないということは、外部から書き換えられることを前提にしていると推論されるべきです。

つまり、読み手は常に「外部からフィールドが書き換えられるかもしれない」ことを念頭にコードを解釈しなければいけません。これはletconstの話と同じで、外部から書き換えられる可能性というのは読み手にとって負担になります。よって、本当にそれが必要でなければ外部から干渉できないようにすべきです。むしろ、外部から書き換えられないのにその可能性が残されているコードというのは読み手を裏切るコードであるとすら言えます。

上のコードについて言えば、presentCountを外部から書き換えられると、上で説明した意図を逸脱することができますよね(uhyo.presentCount += 100とか)。読み手からは、書き手の意図を読み取るのが困難です。つまり、それがやってはいけないことなのか、あるいはやってもいいこと(プレセント増量キャンペーンとか)なのか判断することは難しいでしょう。理解できないことがあると、コードの読解は当然難しくなります。

今回の場合はpresentCountをprivateにして、プレゼント増量キャンペーンをやりたい場合はそれ用のいい感じの設計を考えるべきでしょう。presentCountをprivateにするとこんな感じになります(JavaScript・TypeScriptでは#を使ってprivateなフィールドを表現できます[3])。

class Human {
  name: string;
  age: number;
  #presentCount: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.#presentCount = 0;
  }

  grow() {
    this.age++;
    this.#presentCount++;
  }

  getPresent() {
    if (this.#presentCount <= 0) {
      throw new Error("You have no present!");
    }
    this.#presentCount--;
    return "present";
  }
}

まとめ

この記事では、「読みやすさ」の観点からいくつかの例を考察し、コードの品質について議論しました。それぞれの話題は比較的メジャーな話題で色々な観点から考察できるものですが、この記事では一貫して「書き手の意図」「読者に与える情報」といった観点で考えました。

これらの考察を通じて、「読みやすさ」をベースとする考え方が色々な問題に対して広く適用できるものであることが理解いただけたかと思います。

また、この記事では読み手が“考える”・“推論する”立場にあることを強調しています。コードを読みやすく書くのは書き手の責務であると同時に、書き手の意図を正しく読み取るのは読み手の責務なのです。まるで推理ゲームのように、両者が合理的に推論すればするほど(この記事の観点での)コードの品質は高まると言えます。ですから、読みやすいプログラムを書く書き手を目指すのはもちろん、合理的な考え方が出来る読み手もまた目指すべき目標なのでしょう。

繰り返しになりますが、この記事で述べた考え方は「コードの品質」に対する考え方の一つでしかありません。いろいろな観点・指標が共存して然るべきでしょう。しかし、筆者はこの記事の考え方は広く通用しやすいと思っています。共感いただいた方はぜひシェアしてみてください。

脚注
  1. 筆者は静的型のことを型と呼ぶ流儀を採用しています。JavaScriptなどは型のない言語です。 ↩︎

  2. 一応protectedもあります。 ↩︎

  3. 歴史的経緯からprivate presentCount: number:のような書き方(いわゆるsoft private)もありますが、本文の書き方(いわゆるhard private)の方を筆者としては推奨します。 ↩︎