リーダブル・コードを書く

ここ数年間をプログラミング的な観点で見ると、私が望んでいたほどには面白みがなかったと言わざるを得ません。このことは、恐らく他のプログラマの皆さんも同意見かと思います。そこで、私はこの期間をある意味、充電期間と捉えて、自分の開発ツールの強化に取り組んできました。そして土曜日になると、Bashを使ってワークスペース作りに精を出していたのです。

最後にシェルを使って真剣にプログラミングに取り組んだのは、かれこれ恐竜がまだ地球を支配していた頃だったでしょうか。何年も触れていなかった言語を改めて取り上げ、その昔に自分が書いたコードを見直してみると、いかに自分が成長したかということを実感できて、なかなかに面白いものです。

14年前、私は”コンパクトなコードは優れている”という考えに随分と傾倒していました。コードが少なければ、そしてDon’t Repeat Yourself(DRY)に従えば、バグも大幅に少なくなるという考えです。そんなわけで、当時はかなり、きっちりかっちりとプログラムを作っていましたが、一方で、それはほとんど読めたものではありませんでした。なぜなら、その頃の私は、プログラミングの最も基本的なルールを理解していなかったからです。

プログラムは、人々がそれを読むために書かれるべきである。たまたま、それが計算機で実行できるにすぎない。
– 『計算機プログラムの構造と解釈』

失敗を通じて、上記の内容を嫌というほど思い知らされたのは、私だけではないでしょう。この記事では、どのようにして私が、同僚でも理解でき、時には完全に読めるようなコードを書けるようになったかについて、お話ししたいと思います。

1ステップずつ考える

プログラムの本質は、問題を解決するための指示のセットであると言えます。コンピュータであれば、全体を一度に処理できますが、残念ながら私たちは人間です。全てを管理できるようにするため、コンピュータと自分自身が相互に理解可能な形に、これらの指示を分解しなければなりません。

この相互理解がうまくいかなくなった時、つまり、頭の中にあるモデルの動きが実際のプログラムの挙動と異なってしまった場合に、バグが発生します。

大抵のバグの発生は、あまりにも多くの問題を一度に処理しようとして、その複雑さが自分の頭の許容範囲を超えてしまうことが、その原因です。保存するべきサイズが大きすぎて、自分の脳内記憶装置がある時点で限界に達したわけですね。これらを踏まえ、私は赤ちゃんがものを覚えるように、1ステップずつ考えながらプログラムを作るようになりました。

正にこれこそが、コンパクトなコードと言うべきものです。

メソッドやクラスを決められた行数に収めることがゴールではないですよ。それらは単にポカヨケであり、解決対象の問題を、脳が扱えるサイズにまで分解する原動力となるものに過ぎません。

こうして私は次のような細かいメソッドを書くようになりました。

function in_file {
   local file="$1"
   local content="$2"
   grep -q "$content" "$file" 2>/dev/null
}

これらのメソッドは、より大きなメソッドでも使用できます。

function save_setting {
   local file="$1"
   local key="_setting_$2"
   local value="$3"
   local new_setting="export $key=\"$value\""

   if in_file "$file" "$key\="; then
       replace_in_file "$file" "$key\=" "$new_setting"
   else
       append_to_file "$file" "$new_setting"
   fi
}

in_fileは、容易に1行のコードに置き換えることができますが、それがポイントではありません。grep -q <filename> &>/dev/nullは確かに機能しますが、in_file <filename>は目的を示しています。

同様にreplace_in_fileappend_to_fileについてもソースは提示していませんが、前述のように1行のコードに置き換えることは可能です。

このパターンは、呼び出しチェーンでも使います。

function run_set {
   local key="$1"
   local value="$2"

   is_valid_setting "$key" || die "unknown setting: $key"
   save_setting "$SETTINGS_FILE" "$key" "$value"
}

それぞれが1つのジョブを持つたくさんの細かいメソッドが、1つのジョブを持つ他の細かいメソッドを呼び出すという技術は、Single Responsibility Principle(SRP)と呼ばれ、ソフトウェア設計において、最も重要なコンセプトであるとされています。

時には、メソッドやクラスに対して、どの程度の責務を与えたかが分からなくなることもあるでしょう。これは経験則ですが、接続詞(”そして”あるいは”または”)を使わなければ説明できないような機能の場合、大抵は責務が過剰になっていると言うことができると思います。

目的なくして殺人は証明できない

このアプローチの素晴らしいところは、記述途中のどの段階でも、コードはコンパクトで、読みやすく、非常に理解しやすいということです。かつ、その性質上、DRYでもあります。関数は波線括弧内の文字よりもむしろ目的に基づいており、目的が同じならいつでも再利用できます。

上記のプログラムでは、is_valid_settingが複数の箇所に使われていますね。

1箇所から関数を呼び出すのは問題ありません。この場合、save_settingとin_file共に1箇所で呼び出されます。ただ、これらを(それぞれに1つのジョブとして)別のメソッドに分けてやると、コードがより読みやすく、合理的にもなるでしょう。

このような方法でプログラミングするようになってからというもの、ほとんどバグなしに大量のコードを一気に記述できるようになりました。自分が作ったテストスイートの全てのセクションで問題が検出されないというのは、見ていて本当にえも言われぬ気分になりますよ。それぞれの段階が細かく区切られているおかげで、プログラムの実行中、コンピュータが次に何をするかを、随時、頭の中で追うことができ、テストと修正のループにはまり込むこともなくなりました。

(ちなみにRubyとPythonのコードについては、一般的にメソッドを5~8行の範囲内に収めるのが適切と推奨されています。それが概ね、人の記憶能力が対応できる最大サイズというわけですね。)

私自身のことではない

コンピュータがそれを実行しないのなら、そのコンピュータは壊れているということだ。人がそれを読めないと言うなら、それはコンピュータがじきに壊れるということだ。
-Charlie Martin

例えば、ろくでもない哀れな人間が私のコードに取り組まねばならなくなるだろうと想定してみましょう。その時にベストなのは、この人は深刻なアンガーマネジメントの問題を抱える精神病質者なのだと思い込むことです。この人は、斧を持っています。シャーマン中戦車も持っているかもしれません。そして、私の住所を知っています。

とにかく、それは超人ハルクだということにしましょう。

私は、ハルクが私を見つけ出さなくても私のコードを理解し、うまく変更できる可能性が高くなるように作業したいと思います。こうすることで、私が生き残り、次の誕生日を迎えられる可能性が高まるのです。

これはウィンウィン(Win-Win)の関係です。

そのためには、関数や変数にできるだけ明快な名前を付ける必要があります。
もちろん、関数や変数のネーミングは主観的なものです。客観的な基準はありませんし、これからも確立されることはないでしょう。私は、人間が理解できるようにコードを書いていますが、2人の人間がコミュニケーションを行うための完全に客観的な方法はありません。

したがって、ネーミングの正しい方法はありません。上記のコードで、in_filefile_containsfile_has_stringと呼んでも不思議はありませんし、他の名前も6つくらいは考えられます。ポイントは正しい名前を見つけることではなく、ただ単にこのソリューションに対する私のメンタルモデルをできるだけ明快なやり方で試行し、表現することです。

私が考えていたことをハルクが理解できるようになる可能性がたとえ80%しかなくても、それはつまり、私が次のバースデーケーキを食べられる可能性が80%あるということです。

残念ですが、常にベストの名前を選択できるわけではありません。そんなことは絶対に無理です。そこで、たとえ明快な名前を付けられなくても、少なくとも一貫性を保とうとすることはできます。同じデータを保持する変数は、コードベース全体で同じ名前にします。そうすればハルクは、Frobnitzがユーザー指定のオプションを保持していることを理解する時に、かっとなることもありますが、コードベース全体を一度理解すれば済むのです。私にとっては、無料で太陽を一周できる旅行に匹敵します!

そして、私が冒険したい気分の時は、ハルクに電話をかけて、彼に来てもらい、私の書いたコードを理解できるかどうか教えてもらうことさえするでしょう。彼を無理矢理、オフィスのいすに座らせるという悪夢に耐えられるなら、いっそのこと、私のコードに怒りを覚えないで済むように、リアルタイムでペアプログラミングをするでしょう。

あなたが入れるコメントは正気ではない

何年か前、とうとう私はコメントを入れずにコードを書いてみることにしました。そしてそれは可読性の高いコードを書く上で大きな飛躍でした。もはやコメントに頼ることができないのなら、理解しやすいコードをとても上手く書くしかありません。

私は、コードが何をしているのか、またコードがそれをどのように処理しているのかを説明するために、コメントを使用していました。しかし、コードを変更するたびにそれらのコメントが古くなるのは避けられず、コメント自体が邪魔になることは十分明らかです。

そうは言っても、”コメントはもう二度と書かない!”と言う人たちは何かとてつもなく大事なことを見失っています。というのは、”どうなってるの?”という質問に答えるためのコメントの時間と場所がまだあるからです。

コメントは精神異常を記録する場所です。

仮の話をしましょう。私はいくつかのXMLデータをパースする必要があり、エンティティ(”&amp”のようなもの)は放っておく必要がありました。後で処理するためです。そして残念なことに、XMLライブラリには別の考えがありました。

# WORKAROUND: replace_entities is ignored Apache Xerces, and
# still produces errors with libxml, so we're just going to
# butcher things with regex substitution on the way in, and
# de-butcher them on the way out.

def self.escape_entities(string)
   string.gsub(/\&([\w\-]+)\;/, '$$\1$$')
end

def self.unescape_entities(string)
   string.gsub(/\$\$([\w\-]+)\$\$/) { "&#{$1};" }
end

もちろん、これを説明するためにメソッド名のセットを考えることは可能かもしれませんが、確実に上記のコメントよりもはるかに分かりづらくなるでしょう。

コメントは、難解なアルゴリズムを実装した時や、自分自身でさえ完全には理解できないような新しいテクニックを展開した時にはやはり便利です。アルゴリズムへのリンクや賢いハックを説明しているブログの投稿へのリンクを併記したコメントは、急に大量のハルク・スマッシュを浴びて苦しまないようにするための最善の方法です。

それでは少し私に関係することです

ハルクの他に、もう1人プログラマがいます。それは未来の私です。可読性を求めるコードを書き始めたことはすごく幸運です。

最近私は、特に興味のあるプロジェクトの1つに取り組んでいるのですが、約4年前から準備してあったCoffeeScriptを引っ張り出して、もう一度利用する必要がありました。これは、テキスト選択に関連するクロスブラウザの意地の悪さに対処するためのコードです。

正直に言ってこのコード自体は最悪ですが、簡単に理解でき、可読性も高いのは救いです。私はそれをクリーンアップして、ほとんど苦もなく新しいプロジェクトと統合することができました。

過去の私が可読性を一番に考えた結果、現在の私の頭痛の種が減ったのです。

冗談は抜きにして、これが全てです

1ステップずつ考えるということは、一度に1つのことを行い、意味を基に命名し、末期の精神障害者のための監禁所としてコメントを使用することです。

単純な概念、それは記事にする価値はほとんどありませんが、プログラミングに対する私の考え方を一変させました。Bourneシェルのように面倒なコーディングにも、楽しみを見つけられるようになったのです。