3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Okinawa.rbAdvent Calendar 2018

Day 8

プログラムにおける起承転結

Last updated at Posted at 2018-12-08

物語の構成として「起承転結」を意識するのが良いとされていますが、プログラムにもそういう構成の“お約束”があるのではないか? そんなお話です。

プログラム 上から書くか 横から書くか

今回のターゲットは「関数の中」です。
皆さん、関数の内部処理を書くとき、どんな順序で書きますか?
他の開発者とそういう話をしたことはありませんが、何となく「処理の順を追って上から書く」方が多いんじゃないかな?という気がします。

例題

与えられた会員情報 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で書きましたが、基本的な考え方は他の言語にも適用できるかと思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?