はじめに
わかりやすいコードを書くことはソフトウェア開発において大切なことです。では、具体的にわかりやすいコードとはどんなものでしょうか?その観点はいろいろなものがあります。その中で今回はifとreturnの使い方に注目します。
ifとreturn
プログラミング言語とは、コンピューターの作業の処理手順を書くためにあります。その処理手順は複数にわかれています。その複数の処理手順を順番に実行していくことでコンピューターは作業をこなしていきます。
プログラミング言語にはいろいろな処理手順を書くためにifとreturnと呼ばれる機能があります。ある処理手順をある時だけ実行したい場合には、ifを使います。その時以外はその処理手順は実行しません。また、続きの処理手順があるがその時点で実行を中断したい場合には、returnを使います。続きの処理手順は実行しません。ifとreturnを組み合わせることで、ある時だけ実行を中断することができます。
ifやreturnを使うことで、特定の処理手順を実行したり、実行しなかったり、あるいは実行を中断することができるようになります。これにより、処理手順を順番に実行するだけにとどまらず、いろいろな処理手順をプログラミング言語として書くことができます。具体的には、「あの場合にはこれを実行して、その場合にはここで中断して」というように処理の流れを作ることができます。
以上の説明を元にして用語を整理します。今回の記事では、処理手順を「コード」と呼ぶことにします。そのコードの流れのことを「コードパス」と呼ぶことにします。
ifとreturnの使い方次第でコードパスは自由に変えることができます。例えば、次の2つのコードは処理内容は同じですが、コードパスが異なります。
ifを使った場合:
if <ある場合>
<処理手順>
end
ifとreturnを使った場合:
return if <ある場合でない時>
<処理手順>
コードパスは、コードを読む時の流れでもあります。コードは読まれるものであり、わかりやすさが大切です。そうなると、わかりやすいコードを書くためにはわかりやすいコードパスが大切ということになります。今回の記事ではどのようなコードパスにしたらわかりやすくなるかについて説明します。
コードパスを意識する
自然な流れに沿っていないコードパスになっているとわかりにくくなります。コードを読む側の立場に立ち、コードパスをしっかりと意識する必要があります。
メソッドごとでみたときにそのコードパスが自然な流れかを考えるとわかりやすくなります。メソッドごとでみる理由は、人が文章を読む時に文ごとに理解していくように、コードを読む時はメソッドごとに理解していくからです。
returnを使ったほうが良い場合
悪い例:
def add_comment(post, user, text)
if post.hidden_from?(user)
report_access_error
else
comment = Comment.create(text)
post.add(comment)
end
end
良い例:
def add_comment(post, user, text)
if post.hidden_from?(user)
report_access_error
return
end
comment = Comment.create(text)
post.add(comment)
end
ここでのメソッド名はadd_commentとなっています。コードを読む時、まずはメソッド名を読みます。なので、まずメソッド名から「コメントを追加する」ためのメソッドであることを意識しながら、次にメソッドの定義を読んでいきます。この時点でコメントを追加するコードが大切だと推測しています。
悪い例ではつまづいてしまいます。メソッド名から大切だと推測したコードがぱっと見当たらず、よく読むと実際にはifのelseの中に追いやられているからです。大切でなければならないコードとそのコードの実際の扱われ方が一致していないために、わかりにくいコードになっています。
良い例では、大切なコードをelseから出し、returnで中断されない限り必ず実行されるようにコードパスを変え、大切なコード相応の扱いにしています。
ifを使ったほうが良い場合
悪い例:
def prepare_database(path)
if not File.exist?(path)
return Database.create(path)
end
Database.open(path)
end
良い例:
def prepare_database(path)
if File.exist?(path)
Database.open(path)
else
Database.create(path)
end
end
ここでのメソッド名はprepare_databaseとなっています。まずメソッド名から「pathにあるデータベースを使うために用意する」ためのメソッドだと読み取ります。
悪い例ではつまづいてしまいます。なぜならば、メソッド名から推測されるコードの大切さと実際のコードの扱いが違うからです。なぜかpathにあるファイルが存在しないとデータベースを作り中断しています。データベースを用意するメソッドとしては、データベースを作るのは問題無いはずであり、あえてファイルが存在しない時に限ってデータベースを作って中断する必要はありません。このままでは、あえてこのようなコードパスになっている理由を勘ぐってしまいます。
良い例では、ファイルが存在する場合と存在しない場合の2通りあるデータベースを用意するという処理を対等に扱っています。このメソッドの場合、データベースを開く場合と作る場合は対等の扱われるべきであり、その観点からいえば、開くためのコードをifに入れるかelseに入れるは重要ではありません。しかし、次の3つの補助的な理由により良い例のように開くためのコードをifに入れた方がいいでしょう。
-
このメソッドは、データベースがあろうがなかろうが関係なしにデータベースを用意するためのメソッドです。そこを抽象化して、このメソッドを使う側はデータベースを透過的に扱えるようにしています。このメソッドを深読みすると、このメソッドが何度も使われるなかで、いつもは存在しているデータベースを用意するけど、もしなかったら作ってから用意する(初回起動時など)ということもわかります。つまりは、存在しているデータベースを開いて用意する場合が多いと考えられます。よって、よく通るコードパスは
elseではなくifの中に入れたほうがいいでしょう。 -
ifの中にデータベースを作るコードを入れる場合は、ifの条件は、if not File.exist?となり、notを入れなくてはいけません。基本的には、notを使ってまでコードパスを変える必要があるのは条件がわかりにくくなるデメリットを上回るくらいに、ifにこのコードを入れたいという強い意志をコードに込める必要がある時だけです。今回はそうではないので、notを使わず、より分かりやすい条件になるようにしたほうがいいでしょう。 -
ifにデータベースを作るコードを、elseにデータベースを開くコードを入れて、初めて実行されるコード、2回目以降に実行されるコードと、順番を合わせた方がいいという意見があるかもしれません。たしかに、その観点からのifとelseの順序はあっているように思えます。しかしその反論としては、コードをメソッドごとに読むとき、順序がそうなっているのはあまり気にしません。そうしたくなるのは書く側の立場であり読む側の立場ではありません。読む側からしてみればデータベースを用意する時にいつもすることは何なのかが気になり、それはデータベースを開くことです。それをしっかり伝えるために、ifとelseのコードを実行の順序に合わせる必要はないでしょう。
ifを使い過ぎない
ifを使うと、コードパスに別の流れが作られます。さらに別のifを使うことで、さらに別の流れが作られます。コードパスの流れが大きいとき(ifのコードが長い)やまた流れが多いとき(ifが多い)、コードはわかりにくくなります。
流れの大きさや多さを極力抑えることでわかりやすいコードになります。コーディングスタイルとしてインデントが深ければ深いほど分かりにくくなるのでそれを避けるというのはよくあります。それはこのことです。
悪い例:
def mark_object(collector, object)
if not object.marked?
if collector.owning?(object)
heap_list = collector.ensure_heap_list
if heap_list.in_current_heap?(object)
instance_variables.each do |instance_variable|
mark_object(collector, instance_variable)
end
object.mark
end
end
end
end
良い例:
def mark_object(collector, object)
return if object.marked?
return if not collector.owning?(object)
heap_list = collector.ensure_heap_list
return if not heap_list.in_current_heap?(object)
instance_variables.each do |instance_variable|
mark_object(collector, instance_variable)
end
object.mark
end
悪い例ではつまづいてしまいます。ifが多くて大きいからです。このメソッドで大切なのはobject.markですが、それがifの中へ中へと押し込まれています。ちなみに、大切なコードがelseではなくifの中にあるという観点では良いコードです。
良い例では、object.markをifの中にまったく入れていません。そうするために、object.markをする必要が無い場合になったらメソッドから抜けるようにifとreturnを使ってコードパスを変えています。
エラーの時や特別な時はreturnを使う
エラーの時
悪い例:
def add_comment(post, user, text)
if not post.hidden_from?(user)
comment = Comment.create(text)
if commnet.valid?
post.add(comment)
else
report_invalid_comment(comment)
end
end
end
良い例:
def add_comment(post, user, text)
return if post.hidden_from?(user)
comment = Comment.create(text)
if not comment.valid?
report_invalid_comment(comment)
return
end
post.add(comment)
end
大切なコードが想定する前提と違う場合になるのは、エラーとなります。悪い例では、エラーを拾うためにifを使って、本来大切なコードをifの奥深くにしまいこんでいるためわかりにくいです。
エラーになった場合には、returnを使って本流のコードパスからわかれたエラー用のコードパスを用意するとわかりやすくなります。
ifとelseとはほぼ対等に扱いたいときに使うコードパスです。大切なコードとエラー処理のコードは、悪い例のように対等に扱われるべきコードでしょうか?そうでは無いはずです。大切なコードとエラー処理のコードの扱い方にはreturnを使ってしっかりとした差をつけるべきです。
また、悪い例と比較し良い例ではエラー処理をする必要がある場所に気づきやすくなります。悪い例では暗黙的にあるelseがエラー処理の場所ですが、良い例では明示的にあるreturnがエラー処理の場所だからです。これも良い例の分かりやすさの一つです。
特別な時
悪い例:
class Item
def banner(user)
if Time.now >= Config.urgent_clearing_sale_day # TEMPORARY CODE; REMOVE THIS AFTER THE SALE
"99% OFF!!!"
elsif discount?
"10% OFF!"
elsif recommended?(user)
"Recommended"
else
nil
end
end
end
良い例:
class Item
def banner(user)
if Time.now >= Config.urgent_clearing_sale_day # TEMPORARY CODE; REMOVE THIS AFTER THE SALE
return "99% OFF!!!"
end
if discount?
"10% OFF!"
elsif recommended?(user)
"Recommended"
else
nil
end
end
end
この例ではショッピングサイトを想定しています。"99% OFF!!!"はかなり特別なコードです。コメントからわかるように一時的に追加されたコードであり今後削除されるコードです。つまりは、運用上の一時的な対応のためのコードであることが推測できます。
特別なコードが普通のコードに混じっているとわかりにくいです。
特別なコードは特別なコードとして、それ専用の特別なコードパスを用意してあげるとわかりやすくなります。また、特別なコードを追加した時、特別なコードを削除する時にも、コードレベルで普通のコードと明確に分離しているので、コードの変更時にもミスをしにくくなります。
まとめ
今回はifとreturnの使い方について、それらの組み合わせからできるコードパスは大切であり、わかりやすくするためのifとreturnの使い方を説明しました。
具体的な使い方として、コードパスを意識し、ifを使いすぎず、エラーの時や特別な時はreturnを使えばわかりやすくなるということを説明しました。