かたちづくり

つれづれに、だらだらと、おきらくに

読みやすいコードとは何か

Twitterでお題をもらったので、力不足を承知で書き綴ってみます。プレッシャーで変な汗が出そうですが、頑張ります。(今日は体調を崩してしまったので、自宅でのんびりしながら書いています)

まず「読みやすいコード」という言葉が僕はあまり好きではないのです。「読みやすい」というのは主観的な要素も強く、読みやすさの指標もハッキリとしません。読みやすさは読み手のスキルに依存してしまうという面もあります。そういった曖昧さを排除し、「読みやすさ」の正体をもう少し分解して捉えることは出来ないのでしょうか。

コードを読む、とはどういう行為か

f:id:u_1roh:20120209150357p:plain
脳内にある論理構造をコードに落とす行為がコーディングです。逆にコードから論理構造を脳内に再構築する行為が「コードを読む」ということです。言うなれば、コーディングはシリアライズで、コードリーディングはデシリアライズなわけです。つまりコードが「読みやすい」とは、コードから論理構造へデシリアライズしやすいこと、と捉えることができます。
一般にシリアライズよりもデシリアライズのほうが面倒で難しいです。ですからファイルフォーマットは大抵「読みやすい」ように設計されます。それと同様に、コーディングも「読みやすい」ように行われるべきでしょう。

読みやすさの3つのレイヤー

読みやすさには概ね3つの観点があるように思います。

  1. 趣味や慣れのレベル
  2. ヒトの認知・認識のレベル
  3. 本質的な論理構造のレベル

インデントを揃えるとかどこにスペースを入れるとか、いわゆるコーディングスタイルは 1 のレイヤーに属します。ここでは議論しません。
2 は少し本質的です。ヒトの脳は一般にどのように対象を認識するか、という観点を指しています。各個人に依存した趣味や慣れの問題ではなく、人類の脳に共通な観点です。例えば「良い名前をつけるべき」というのは2のレイヤーに属します。人間なら誰だって、適切な名前が付けられている方が読みやすいでしょう。しかし名前は論理構造とは何の関係もなく、クラス名や変数名を変えたところで動作結果に差異は出ません。名前はコードを読み解く際の「ヒント」であって、もちろんそのヒントはとても重要なものですが、論理構造そのものとは関係がありません。
3 が最も本質的なもの、と僕は考えています。ここでは可能な限りこの観点で書いてみたいと思っていますが、うまく書けるかどうか・・・。

【余談】オブジェクト指向について

やや余談になりますが、オブジェクト指向は 2 のレイヤー「ヒトの認知・認識のレベル」で語られ過ぎたように思っています。これはオブジェクト指向という言葉がプログラミングから分析・設計まで幅広く使われたことにも原因があると思われます。世界の万物をオブジェクトとしてモデル化するだとか、メッセージパッシングだとか、名前がないものは認識できないとか、名前で対象を切り取るとか、そういう哲学的な語られ方をすることが多かったように思います。こういった議論は、何かものすごく深遠な真実に触れたような気にさせられる割に、プログラミングにはさほど役に立ちませんでした。(分析や設計では役立つ面もあったかもしれませんが、私には分かりません)
オブジェクト指向プログラミングについては、それとは違う言葉でメリットが説明されるべきだと考えています。

長いコードは何故ダメか

下記のコードを例に考えてみましょう。

int hoge()
{
  int a = 1;
  int b = 2;
  int c = a + 3;
  int d = b + 4;
  return c + d;
}

この関数は5行あります。一般に、各行は潜在的にそれより前の行に依存している可能性があります。つまり潜在的な依存関係は下図のようになります。
f:id:u_1roh:20120209170513p:plain
つまり n 行の関数があったとすると、各行の間には n(n-1)/2 の依存関係が潜在的に存在しうることになります。
しかし上記のコードを実際に読み解いてみれば、下図のようなシンプルな依存関係で構成されていることが分かります。
f:id:u_1roh:20120209172218p:plain
このように、潜在的に存在しうる依存関係から実際の論理構造を抽出する行為が「コードを読む」ということだと僕は考えています。そして、潜在的な依存関係を出来るだけ小さく抑え、実際の論理構造に近い形に保たれているコードが「読みやすいコード」だと考えています。
上で見たように、潜在的依存関係は行数の二乗のオーダーで膨らんでいきます。これが長い関数は読みにくい原因です。

無名を好め

名前をつけるということは、他から参照され得るということです。つまり潜在的な依存関係を増やします。コードの読み手は「もしかするとこの名前は他の場所からも参照されているかもしれない」という可能性を念頭に置きながら読む必要が生じます。ですから、そもそも名前を付けずに済むのならば名無しで済ませたほうが良いはずです。
下記の2つのコードは等価なものですが、前者は関数の戻り値を受けた変数として a, b という名前を付けています。私は変数名を付けていない後者のコードのほうが優れていると考えています。

int a = foo();
int b = bar();
buzz( a, b );
buzz( foo(), bar() );

とはいえ、さすがに下記のコードはやり過ぎかもしれませんね。

func1(
  func2( func3(), func4( func5(), func6() ), func7() ),
  func8( func9(), func10() ) );

どこからが「やり過ぎ」と感じるかは個人差があります。これは人間の脳の能力とか個人の慣れとか好みの問題が絡むからです。おそらく訓練されたLispプログラマはこの程度ではびくともしないでしょう(たぶん)。

名前をつけるならスコープはできるだけ小さく

当然、名前を付けざるを得ないシチュエーションは存在します。クラス定義、メソッド定義、変数宣言、すべてが名前をつける行為です。これらすべての名前をつける行為は、名前空間の汚染につながることを理解するべきです。ここでいう名前空間の汚染とは、単に名前の衝突可能性のことを指しているのではありません。潜在的依存関係の増大を指しています。
あるスコープに名前が n 個定義されていると、それらの潜在的依存関係は n の二乗のオーダーになります。つまり名前を定義するということは、潜在的依存関係を増大させ、読みにくくする行為であるということです。
ですから、その影響範囲は出来る限り小さく抑えたほうが良いということになります。可能な限り小さなスコープに名前を閉じ込めるべきです。変数名、メソッド名、クラス名、全てにおいて名前を出来るだけ小さなスコープに閉じ込めることが読みやすさにつながります。



・・・息切れしてきた。
やばい、すごい大変な気がしてきた。全然説明しきれん。まだ静的な構造のことしか書けてなくて、動的な状態遷移の読みやすさについては一言も触れてないし。
しかも、当たり前のことしか書いてない気がする。書いてるうちに脳内で「そんなの当たり前じゃん」ていう声が聞こえてきてヤバイ。
何よりヤバイのは誰に向けて書いているのか分からなくなってきたこと。こういうのは初学者向けに書かなきゃいけないんだろうけど、もう俺の頭ン中は「上級者からのツッコミが怖い」という恐怖心でいっぱいだ。
なんだか書いてる意味があるのか分からなくなってきたよ、パトラッシュ。
尻切れトンボで申し訳ないけど、一旦ここで終わりにするよ。