物語の構成として「起承転結」を意識するのが良いとされていますが、プログラムにもそういう構成の“お約束”があるのではないか? そんなお話です。
プログラム 上から書くか 横から書くか
今回のターゲットは「関数の中」です。
皆さん、関数の内部処理を書くとき、どんな順序で書きますか?
他の開発者とそういう話をしたことはありませんが、何となく「処理の順を追って上から書く」方が多いんじゃないかな?という気がします。
例題
与えられた会員情報 member から年齢 age を取得する get_age 関数を例に考えてみます。
memberにはnilが渡される可能性があり、その場合はnilを返すものとします。
def get_age(member)
# どう書く?
end
p get_age({:name=>"Bob", :age=>15}) # => 15
p get_age(nil) # => nil
回答例1.上から書く
やるべきことを頭から一気に書いていくと、例えばこのようなコードになります。
def get_age(member)
if member
return member[:age]
end
end
この書き方は、小説で言えば1ページ目から出来事を順に書いていくやり方と言えるでしょう。
回答例2.結論から書く
私なら、結論から書きます。ちなみに「横から書くか」という言い回しはもちろんネタなので、横から書く方法は分かりません(そもそも横って何だ)。
def get_age(member)
age = nil
return age
end
まず、get_ageなのですから、最後にageをreturnします。
メソッドでやるべきことは基本的に一つであり、やるべきことがメソッド名になっているべきです。これは、コードの書き方に関する本等で表現に違いはあれどよく言われていることかと思います。
そして次にメソッドのコメントを書きます。
開発環境/エディタによっては"@param"等を補完してくれるのでこのタイミングで書いています。
# 年齢を取得する
# @param [Hash] member 会員情報
# @return [Integer|nil]
def get_age(member)
age = nil
return age
end
そしてやっと、処理のメインを書きます。
# 年齢を取得する(以下略
def get_age(member)
age = member[:age]
return age
end
今回は1行で完結する単純なロジックなのでage = nil
の部分を書き直す形になりましたが、複数行を費やすようならこんな感じで残したりします。
# 関数内部のみ
age = nil
birthday = Date.parse(member[:birthday])
age = MyUtil.diff_year(Date.today, birthday)
return age
そして、入力値のチェックは後から追加します。
# 年齢を取得する(以下略
def get_age(member)
if member.nil?
return nil
end
age = member[:age]
return age
end
結果、関数内1〜3行目は「入力値チェック」、4行目は「メイン処理」、5行目はそもそもRubyだと書く必要がありませんが、返り値を明確にするためあえて記述するのが個人的には好みです。
それはさておき、こんな感じで関数の内部に明らかな構造が生じました。
起承転結ではありませんが、だいたいどの言語で書いてもこのような構成にならないでしょうか?
ちなみに、入力値チェックは処理の先頭でなくてもよいと考えています。
私の場合、「内部処理を最後まで進むのが正常系であり、異常系は随時途中でハネる」というポリシーで書いています。
その恩恵を説明するために「上記コードをリリース後、member[:age]に文字列が入ってくるケースがあることが分かった」と仮定しましょう。
コードで表現すると、こんな感じです。対応としてはnilを返すものとします。
p get_age({:name=>"Bob", :age=>"-"}) # => nil
入力値チェックを追加する
では修正しましょう。
回答例1からの修正
def get_age(member)
if member
if member[:age]
if member[:age].is_a? Integer
return member[:age]
end
end
end
end
ネストがかなり深くなりました。
書き方を工夫すれば多少は改善します。
def get_age(member)
if member
if member[:age] and member[:age].is_a? Integer
return member[:age]
end
end
end
簡単な例だからまだ分かりますが、長くなったり条件分岐が増えてくると、正常な時にどんな値が返ってくるのか? さらに修正を加えることになったらどこに加えるべきか? 判断するのが加速度的に難しくなっていきませんか?
回答例2からの修正
# 年齢を取得する(以下略
def get_age(member)
if member.nil? or member[:age].nil?
return nil
end
if not member[:age].is_a? Integer
return nil
end
age = member[:age]
return age
end
ポイントは「異常系は随時途中でハネる」ため、if member[:age].is_a? Integer
ではなく、if not member[:age].is_a? Integer
としているところです。
これを徹底すると、ネストが深くならずに済みます。
if not member[:age].is_a? Integer
#return nil
raise('Invalid type: age')
end
内容によってはこんな感じで、例外でハネるのもいいでしょう。
全体としてコードは長めになりますが、何がNGでその時に何が起きて、本来どういう処理が行われてというのが分かりやすく、修正時もアタリがつけやすいのではないでしょうか?
まとめ
このように、『やることを順番に書く』のではなく『「宣言(今回の例ではありませんでしたが)」「入力値チェック」「メイン処理」等の構造を意識して書く』とコードが読みやすく、メンテしやすくなってオススメです。
今回はRubyで書きましたが、基本的な考え方は他の言語にも適用できるかと思います。