Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What is going on with this article?
@fullkawa

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

More than 1 year has passed since last update.

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

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

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

例題

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

2
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
fullkawa

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
2
Help us understand the problem. What is going on with this article?