見出し画像

私のプログラムの書き方

前回記事を書いた時よりだいぶ時間が空いてしまいました

現在のプロジェクトが佳境に差し掛かってきたのでここ数ヶ月は忙しくしています


今回はよわよわエンジニアがどんな感じでコードを書いているかを書き殴ってみようと思います

説明に使用する言語はPythonで実装内容としてはよくある閏年判定にしたいと思います


仕様の確認

まずは仕様の確認をします

閏年の判定なので必要な条件をググって以下に貼り付けておきます

wikipedia大先生の閏年のページによると

西暦年が4で割り切れる年は(原則として)閏年。
ただし、西暦年が100で割り切れる年は(原則として)平年。
ただし、西暦年が400で割り切れる年は必ず閏年。

とのことです

今回は引数で受け取った数値が閏年に該当するかをbooleanで返す部品を作っていきたいと思います


疑似コード(Pseudocode)

自分の場合は疑似コードを書きながら実装を行っています

補助輪に頼っているような感じがしつつ今まで実装を行っていましたがPPP(Pseudocode Programming Process)という開発手法があるらしいのでヨシとします

疑似コードはプログラミング初心者の方々によく勧められている方法なのですが、いきなりコードを書き始めないで日本語でどのような処理の流れになるかを書き出すところから始めます

閏年判定の例でいいますと

説明
閏年を判定する
閏年の場合は真を返却し
平年の場合は偽を返却する
引数:数値
返却値:boolean

範囲は西暦1年から3000年までとして、範囲外は例外で落とす
400で割り切れる場合は閏年とする
400で割り切れず、100で割り切れた場合は平年とする
上記以外で4で割り切れた場合は閏年とし、割り切れなかった場合は平年とする

結構まどろっこしいですがこんな感じでババっと書き出しています

もしかしたらPPPのお作法からは外れているかもしれませんが気にしません


テストファースト

テストファーストとはよくテスト駆動設計という言葉を耳にすることがあると思いますが、あれのはずです(適当)

まずはメソッドの雛形を作り、単体テストを書いてからメソッドに肉付けしていくイメージでやっています

def is_leap_year(year: int)-> bool:

 return False

def main():
 for i in range(1, 3001):
   if is_leap_year(i):
     print("{}年は閏年だよ".format(i))
   else:
     print("{}年は閏年じゃないよ!".format(i))

if __name__ == "__main__":
   main()

こんな感じでis_leap_yearを使って1から3000までの年を閏年かどうか調べていくプログラムを作りたいとします

処理はis_leap_yearメソッドに書いていくことになりますが一旦ただFalseを返すメソッドになっています

ここまできたらテストコードを書いていきます

from unittest import TestCase, main
from leapYear import is_leap_year

class LeapYear(TestCase):
   """ test class of leapYear.py
   """

   def test_is_leap_year_expected_true(self):
       """ is_leap_yearメソッド正常系テスト(期待値True)
       """
       test_pattern = [
           400,  # 400で割り切れるため閏年=True
           4,  # 400でも100でも割り切れないが4で割り切れるため閏年=True
       ]
       for arg in test_pattern:
           with self.subTest(arg):
               self.assertTrue(is_leap_year(arg))

   def test_is_leap_year_expected_false(self):
       """ is_leap_yearメソッド正常系テスト(期待値False)
       """
       test_pattern = [
           100,  # 400で割り切れず、100で割り切れるため平年=False
           1,  # 引数の最小値。4で割り切れないため平年=False
           3000  # 引数の最大値。100で割り切れるため平年=False
       ]
       for arg in test_pattern:
           with self.subTest(arg):
               self.assertFalse(is_leap_year(arg))

   def test_is_leap_year_throw_exception(self):
       """ is_leap_yearメソッド異常系テスト(ValueError発生)
       """
       MSG = "0〜3000年範囲外の値が指定されています。year = {}"

       test_pattern = [
           0,  # 最小許容値(1)より小さい値のため例外
           3001,  # 最大許容値(3000)より大きい値のため例外
           -1  # マイナス値で最小許容値(1)より小さい値のため例外
       ]

       for arg in test_pattern:
           with self.subTest(arg):
               with self.assertRaises(ValueError) as cm:
                   is_leap_year(arg)

               the_exception = cm.exception
               self.assertEqual(the_exception.args[0], MSG.format(arg))

if __name__ == "__main__":
   main()

テストコードで仕様が細部に渡るまで全て満たされているか確認します

テストコードを書いている段階で仕様の細かい部分で考慮漏れが発覚することがあるのでその都度確認し、テストコードに起こす作業を行います

テストを実行すると当たり前のようにFailedになります

k@MacBook-Pro note % python test_leapYear.py -v
test_is_leap_year_expected_false (__main__.LeapYear)
is_leap_yearメソッド正常系テスト(期待値False) ... ok
test_is_leap_year_expected_true (__main__.LeapYear)
is_leap_yearメソッド正常系テスト(期待値True) ... test_is_leap_year_throw_exception (__main__.LeapYear)
is_leap_yearメソッド異常系テスト(exception) ... 
======================================================================
FAIL: test_is_leap_year_expected_true (__main__.LeapYear) [400]
is_leap_yearメソッド正常系テスト(期待値True)
----------------------------------------------------------------------
Traceback (most recent call last):
 File "test_leapYear.py", line 19, in test_is_leap_year_expected_true
   self.assertTrue(is_leap_year(arg))
AssertionError: False is not true
======================================================================
FAIL: test_is_leap_year_expected_true (__main__.LeapYear) [4]
is_leap_yearメソッド正常系テスト(期待値True)
----------------------------------------------------------------------
Traceback (most recent call last):
 File "test_leapYear.py", line 19, in test_is_leap_year_expected_true
   self.assertTrue(is_leap_year(arg))
AssertionError: False is not true
======================================================================
FAIL: test_is_leap_year_throw_exception (__main__.LeapYear) [0]
is_leap_yearメソッド異常系テスト(exception)
----------------------------------------------------------------------
Traceback (most recent call last):
 File "test_leapYear.py", line 45, in test_is_leap_year_throw_exception
   self.assertRaises(ValueError, is_leap_year, arg)
AssertionError: ValueError not raised by is_leap_year
======================================================================
FAIL: test_is_leap_year_throw_exception (__main__.LeapYear) [3001]
is_leap_yearメソッド異常系テスト(exception)
----------------------------------------------------------------------
Traceback (most recent call last):
 File "test_leapYear.py", line 45, in test_is_leap_year_throw_exception
   self.assertRaises(ValueError, is_leap_year, arg)
AssertionError: ValueError not raised by is_leap_year
======================================================================
FAIL: test_is_leap_year_throw_exception (__main__.LeapYear) [-1]
is_leap_yearメソッド異常系テスト(exception)
----------------------------------------------------------------------
Traceback (most recent call last):
 File "test_leapYear.py", line 45, in test_is_leap_year_throw_exception
   self.assertRaises(ValueError, is_leap_year, arg)
AssertionError: ValueError not raised by is_leap_year
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=5)


やっと実装

まずは疑似コードを全てコメントとしてコードに落とし込みます

(メソッドの説明部分はちょっと変えてます)

def is_leap_year(year: int) -> bool:
   """閏年を判定する
   Args:
     year(int): 数値型の年。
   Returns:
     boolean: 閏年であればTrue、平年であればFalse
   """

   # 範囲は西暦1年から3000年までとして、範囲外は例外で落とす
   # 400で割り切れる場合は閏年とする
   # 400で割り切れず、100で割り切れた場合は平年とする
   # 上記以外で4で割り切れた場合は閏年とし、割り切れなかった場合は平年とする
   return False

上のような感じであとはコメント毎に処理を記述していきます

def is_leap_year(year: int) -> bool:
   """閏年を判定する
   Args:
     year(int): 数値型の年。(最小値0、最大値3000)
   Returns:
     boolean: 閏年であればTrue、平年であればFalse
   Raises:
     ValueError: 0〜3000範囲外の引数が渡ってきた場合に発生
   """

   # 範囲は西暦1年から3000年までとして、範囲外は例外で落とす
   if year < 1 or year > 3000:
       raise ValueError("0〜3000年範囲外の値が指定されています。year = {}".format(year))

   # 400で割り切れる場合は閏年とする
   if year % 400 == 0:
     return True

   # 400で割り切れず、100で割り切れた場合は平年とする
   if year % 100 == 0:
     return False

   # 上記以外で4で割り切れた場合は閏年とし、割り切れなかった場合は平年とする
   if year % 4 == 0:
     return True

   return False

書けました!

先ほどのテストを実行してみます

k@MacBook-Pro note % python test_leapYear.py -v
test_is_leap_year_expected_false (__main__.LeapYear)
is_leap_yearメソッド正常系テスト(期待値False) ... ok
test_is_leap_year_expected_true (__main__.LeapYear)
is_leap_yearメソッド正常系テスト(期待値True) ... ok
test_is_leap_year_throw_exception (__main__.LeapYear)
is_leap_yearメソッド異常系テスト(ValueError発生) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK

オールグリーンとなりました!


リファクタリング

先ほどのメソッドを見てみると分かりやすいですが少し不格好な気もします

リファクタリングの可能性を探るために自分の場合は以下の点を意識しています

・メソッドが長すぎないか(私の場合は50行くらいを目安にしていますが、人によっては30行とかまちまちです)
・重複がないか
・if文が多すぎないか
・ネストが深くないか(メソッド内でインデント3つ以上はアウト!など自分の基準を作る)
・論理演算子の数が多く、条件が分かりづらくなっていないか

(他にもたくさんある・・・!)

先ほどのメソッドの範囲チェックから下の部分をコメントアウトし、以下のように書き直してみます

def is_leap_year(year: int) -> bool:
   """閏年を判定する
   Args:
     year(int): 数値型の年。(最小値0、最大値3000)
   Returns:
     boolean: 閏年であればTrue、平年であればFalse
   Raises:
     ValueError: 0〜3000範囲外の引数が渡ってきた場合に発生
   """

   # 範囲は西暦1年から3000年までとして、範囲外は例外で落とす
   if year < 1 or year > 3000:
       raise ValueError("0〜3000年範囲外の値が指定されています。year = {}".format(year))

   # # 400で割り切れる場合は閏年とする
   # if year % 400 == 0:
   #   return True
   # # 400で割り切れず、100で割り切れた場合は平年とする
   # if year % 100 == 0:
   #   return False
   # # 上記以外で4で割り切れた場合は閏年とし、割り切れなかった場合は平年とする
   # if year % 4 == 0:
   #   return True
   # return False

   can_divide_400 = year % 400 == 0
   can_divide_4_and_cant_divide_100 = year % 4 == 0 and year % 100 != 0
   
   return can_divide_400 or can_divide_4_and_cant_divide_100

判定処理をコメントアウトした後に追加したのは下二行となります

これでテストが通るか確認します

k@MacBook-Pro note % python test_leapYear.py -v
test_is_leap_year_expected_false (__main__.LeapYear)
is_leap_yearメソッド正常系テスト(期待値False) ... ok
test_is_leap_year_expected_true (__main__.LeapYear)
is_leap_yearメソッド正常系テスト(期待値True) ... ok
test_is_leap_year_throw_exception (__main__.LeapYear)
is_leap_yearメソッド異常系テスト(ValueError発生) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK

テストも通ったので大丈夫そうですね

あとはコメントを適宜処理して完成です

def is_leap_year(year: int) -> bool:
   """閏年を判定する

   Args:
     year(int): 数値型の年。(最小値0、最大値3000)
   Returns:
     boolean: 閏年であればTrue、平年であればFalse
   Raises:
     ValueError: 0〜3000範囲外の引数が渡ってきた場合に発生
   """

   # 範囲は西暦1年から3000年までとして、範囲外は例外で落とす
   if year < 1 or year > 3000:
       raise ValueError("0〜3000年範囲外の値が指定されています。year = {}".format(year))

   # 400で割り切れる場合は閏年とし、Trueを返却
   can_divide_400 = year % 400 == 0
   # あるいは4で割り切れ、かつ100で割り切れない場合も閏年とし、Trueを返却
   can_divide_4_and_cant_divide_100 = year % 4 == 0 and year % 100 != 0
   # 上記以外は平年とし、Flaseを返却

   return can_divide_400 or can_divide_4_and_cant_divide_100


終わりに

シンプルな処理だとテストから書き始めてしまうこともありますが、そんなに頭がいい方ではないので少しでも複雑になりそうな気配があれば疑似コードを書くことにしています

会社の先輩が適当に教えてくれたやり方ではありますが、今も基本的にこのやり方で実装を行っています

やり方としては全然まだまだだと思いますが改善していきたいと思います

なんかあったらコメントいただけると幸いです!

この記事が気に入ったらサポートをしてみませんか?