はじめに
わかりやすいコードを書くことはソフトウェア開発において大切なことです。では、具体的にわかりやすいコードとはどんなものでしょうか?その観点はいろいろなものがあります。その中で今回は名前のつけ方に着目します。
コードに名前をつけるということ
ソフトウェア開発において、名前をつける作業というのは絶えず発生します。メソッド名、変数名、クラス名、ファイル名などなど。名前をつける機会を挙げたらキリがありません。では、そもそもなぜ名前は必要なのでしょうか?
それはソフトウェアに限らず言えることですが、複数のモノを区別したいためです。例えば、まったく違う処理をする別々のメソッドに同じ名前をつけたらソフトウェアは正しく動きません。それを防ぐためにそれぞれのメソッドにちゃんと名前をつける必要があります。それぞれのモノにそれぞれ違う名前をつけて区別できなければソフトウェアはそもそも動きません。
名前をつける作業が必要な理由はもう一つあります。それは、わかりやすくするためです。区別できる名前でさえあれば、確かにソフトウェアは動きますが、それではいけません。名前が被らなければどんな名前をつけてもいいわけではありません。対象を的確に表しているわかりやすい名前をつける必要があります。
ソフトウェア開発において、名前をつける作業には次のようなものがあります。
- 計算結果をローカル変数に代入するとき、計算式にローカル変数名で名前をつけています。
- 条件式をメソッドへくくりだすとき、条件式にメソッド名で名前をつけています。
- 関連しているメソッドをクラスにまとめるとき、メソッドにクラス名で名前をつけています。
- 関連しているクラスを別のファイルに移動するとき、クラスにファイル名で名前をつけています。
これらの作業の中で名前がつけられる対象の種類はかなり違いますが、名前をつけるという点では共通しています。これらの作業では、大なり小なりのコードに名前をつけている作業に他なりません。名前をつける時、開発者は、ソフトウェアの特定部分のコードに対して名前をつけます。
これらの作業をまとめて「コードに名前をつける」という名前をつけることにします。今回の記事ではどのようにコードに名前をつけたら、コードがわかりやすくなるかについて説明します。
略さない
コードに名前をつける時、略した名前をつけるという慣習が存在しています。略した名前というのは、自然言語の言葉を短くした、そのソフトウェア内だけで使われている独自の名前のことです。例えば「manager
」から「mgr
」という名前に略すことです。ローカル変数に言葉の先頭1文字だけの名前をつけたりすることも当てはまります。
たしかにこの慣習は広く用いられていますが、コードをわかりにくくする悪い慣習です。極力避ける必要があります。
略した名前は読みにくい
開発者は略した名前が含まれているコードを読む度に必ず頭の中で元の言葉に読み直しています。人の脳は生物的にそのような読み直しがとても不得意です。視覚情報の再処理は、人の脳にとって負荷が高いからです。
その負荷が高い処理を避けるために、シンタックスハイライトでコードに色をつけ、開発者がラクができるようにしています。他にも、普通のdiffではなくUnified Formatになっているdiffが広く採用されているのも、視覚的に縦に変更が揃えられていたほうがラクだからです。そのように開発者がラクをしている中で、名前をつける際にわざわざ略して人の脳に負荷を高めるのは合理的ではありません。
略した言葉は使いにくい
略した名前をつける時だけ発生する問題があります。さまざまな略し方があるなかで、どう略すか、どう略したかで悩んでしまう問題です。たとえばcontrol
の場合ctr
と略したり、ctrl
と略したりする場合があります。environment
はenv
と略したりenviron
と略したりする場合があります。
また、略した名前よりも略されていない元々の言葉の方が、キーボードで入力し慣れているので、効率よく入力できます。
略されたためにわかりにくいコードの例
名前を略したものにするということは、名前から推測できるはずの情報を意図的に削っているということです。これはコードを部分的に読むときにわかりにくくします。具体的にわかりにくい例をあげてみましょう。例えば次のコードを読んだときに、処理内容を読み取れるでしょうか?
悪い例:
convert(downloaded_archive.first_mtrl)
ダウンロードされたアーカイブの中にある、最初のmtrl
を変換しているということまではわかりますが、mtrl
が何かがわかりません。読んだ時につまづいてしまい非常にモヤモヤします。しまいにはmtrl
が何を意味しているのかを考え始めてしまいコードを読むという本来の作業から脱線してしまいます。
mtrl
が何のことだかわからなくて、まだまだモヤモヤしていますか?答えは、material
です。
つまりは、上のコードがしているのは、「ダウンロードされたアーカイブの中にある、最初の資料を変換する」です。すっきりしたでしょう?このようにモヤモヤさせないようなコードはとても大切です。
以下のように略さず名前をつけるとわかりやすいコードになります。
良い例:
convert(downloaded_archive.first_material)
略すのは悪しき慣習
ソフトウェア開発において、かつてのgoto
やグローバル変数が、今や悪しき文化となったように、略した名前も過去の遺物となるべきです。これからソフトウェアを開発をする際、その慣習に引きずられないように注意する必要があります。そもそも、コードはプログラミング言語と呼ばれる言語の一種です。自然言語が文章中で言葉を略さないように、名前を略さないほうが、むしろ自然であり、つけるべき名前であると言えます。
サボらずこまめに名前をつける
名前がつけられていない長いコードはわかりにくいです。
悪い例:
def hexdump(data)
data.bytes.each_slice(16).collect do |bytes|
bytes.each_slice(8).collect do |half|
half.collect do |byte|
"%02x" % byte
end.join(" ")
end.join(" ").ljust(3*16+1) + " " +
bytes.collect do |byte|
(0x20..0x7e).include?(byte) ? byte.chr : "."
end.join
end.join("\n")
end
実行例:
irb(main):013:0> puts(hexdump(Random.new.bytes(100)))
d5 c4 e1 ad db 87 28 50 d6 17 1b 91 74 6b bb 7d ......(P....tk.}
7c a5 c9 48 1e f7 ed 4b 69 2c cc 6a e6 18 c8 f0 |..H...Ki,.j....
04 ff 9d 47 19 29 57 5a 4e 11 35 5d ca 03 db a6 ...G.)WZN.5]....
a8 c3 ff 40 1d 41 37 a5 f1 fb 72 59 b7 fd da b8 ...@.A7...rY....
58 37 ba 66 5a 2c f7 75 0d fe 82 c6 4c 58 14 c4 X7.fZ,.u....LX..
16 78 59 29 c8 11 2b 63 02 23 0c 73 e9 e2 bc bc .xY)..+c.#.s....
a5 62 84 1c .b..
= > nil
メソッドに的確に名前がついているおかげで、データを16進数形式でダンプすることはすぐにわかります。ですが、実装がどうなっているのかがわかりにくいです。例えば16進数とAscii文字の間に区切り文字として|
を入れたいことを想定してみましょう。悪い例だと名前がつけられていないために、やりたいことは小さい修正なはずなのに、どこをどう変更したらいいのかを知るために全てのコードを理解しなければなりません。
長いコードに名前がつけられていないということは、名前がつけられ抽象化されたより小さいコードに分かれていないということです。どこを変更したらいいのかがわかりにくくなっている理由は、「16進数の部分」を表現しているモノと「Ascii文字の部分」を表現しているモノに対応するコードに名前がつけられていないためです。それらのモノに対応するコードに名前をつけてみます。
def hexdump(data)
data.bytes.each_slice(16).collect do |bytes|
hex = dump_to_hex(bytes)
ascii = dump_to_ascii(bytes)
hex.ljust(3*16+1) + " " + ascii
end.join("\n")
end
def dump_to_hex(bytes)
bytes.each_slice(8).collect do |half|
half.collect do |byte|
"%02x" % byte
end.join(" ")
end.join(" ")
end
def dump_to_ascii(bytes)
bytes.collect do |byte|
(0x20..0x7e).include?(byte) ? byte.chr : "."
end.join
end
これだけでもだいぶhexdump
はわかりやすくなったはずです。区切り文字を|
にするにはどこを変更すればいいのかすぐにわかります。
もう少しだけコードに名前をつけてみましょう。hexdump
はdata
と呼ばれるバイト列を受け取り、16進数形式のダンプに変換してその結果を返しています。変換する際には、受け取ったバイト列を小分けにして順番に取り出しながらダンプ結果の各行へと変換しています。その小分けにされたバイト列はbytes
という名前になっていますが、data
とbytes
はほぼ同義語でありわかりにくいです。その小分けにされたバイト列に的確に名前をつけましょう。data
が小分けされたバイト列をchunk
という名前にします。
def hexdump(data)
dump_each_chunk(data) do |chunk|
hex = dump_to_hex(chunk)
ascii = dump_to_ascii(chunk)
hex.ljust(3*16+1) + " " + ascii
end.join("\n")
end
def dump_each_chunk(data)
data.bytes.each_slice(16).collect do |chunk|
yield(chunk)
end
end
def dump_to_hex(chunk)
chunk.each_slice(8).collect do |half_chunk|
half_chunk.collect do |byte|
"%02x" % byte
end.join(" ")
end.join(" ")
end
def dump_to_ascii(chunk)
chunk.collect do |byte|
(0x20..0x7e).include?(byte) ? byte.chr : "."
end.join
end
こうすることで、最初に複数のメソッドにhexdump
を分けた上で、変数の関係をdata => chunk (=> half_chunk) => byte
と的確に名前をつけ、よりわかりやすくなりました。
このように長いコードには部分部分に的確に名前をつけるとわかりやすくなります。
同じコードには同じ名前をつける
ソフトウェアを変更する時、コードの名前からどこを変更すればいいのかを読み取ります。そして関係していると読み取ったコードを変更してあとは大丈夫だと思ってしまいます。しかし、もし他にも変更しなければならないコードがあるとすれば変更漏れということになり、バグなどの原因となります。同じコードは1つにまとめ、1つだけの名前をつけると、わかりやすくなります。
抽象的でいまいちピンとこないことでしょう。例で説明します。例えば次のコードがあったとします。
def error_message
"use one of following modes: download, upload"
end
def change_mode(mode)
mode = mode.to_sym
raise error_message unless modes.include?(mode)
send(mode)
end
def modes
[
:download,
:upload,
]
end
def download
# ...
end
def upload
# ...
end
ここに新たにmonitor
モードを追加するとします。具体的には、次のようにmodes
メソッドを変更し、monitor
メソッドを追加します。
def modes
[
:download,
:upload,
:monitor,
]
end
def monitor
# ...
end
これだけではバグがあります。すぐにわかるでしょうか?次のようにerror_message
メソッドも変更しなければならないのです。
def error_message
"use one of following modes: download, upload, monitor"
end
なので、monitor
を追加する前から次のようにコードが書かれていることが望ましいです。
def error_message
"use one of following modes: #{modes.join(", ")}"
end
そもそもこれはなぜバグと認識できるのでしょうか?それは開発者の視点からは、ヘルプメッセージから出力されるモードと実際にサポートされているモードは同じであるという認識があるからです。ここで注意してほしいのが開発者の視点からはこの2つの場面でのモードは同じモノを指し示しています。ですがコード上ではその同じのモノが2箇所のコードに対応しています。これがバグの原因となります。繰り返しになりますが、開発者が認識しているモノとコードは1対1で対応しているとわかりやすいです。
ここで仮定を変えてみます。monitor
は隠しモードでエラーメッセージには含めたくないとします。そうなるとerror_message
を修正する必要は無いので、最初の変更だけでバグが無いことになります。
しかしわかりやすいコードという観点からは問題があります。別の開発者が読んだとき、error_message
を修正したほうがいいと思うかもしれません。
monitor
の追加でモノとコードの関係がずれているからです。ヘルプメッセージから出力されるモードと実際にサポートされているモードとは別々のモノであるという認識があるからです。それを次のようにコードから読み取れるようにするとわかりやすくなります。
def error_messge
"use one of following modes: #{normal_modes.join(", ")}"
end
def modes
normal_modes + hidden_modes
end
def normal_modes
[
:download,
:upload,
]
end
def hidden_modes
[
:monitor,
]
end
こうすれば、あとから読んだときに、error_message
にmonitor
が無いのはたまたま抜けていたバグではなくて隠しモードとして扱っているためだから、ということが的確に読み取れます。
まとめ
今回はコードの名前のつけ方を通して、コードをわかりやすくする方法を説明しました。
具体的には、略した名前をつけず、名前をつけるのをサボらず、同じコードには同じ名前をつけるとわかりやすくなるということを説明しました。