PR

ロジックは仕様書を表現するよう実装する

ロジックは仕様書を表現するよう実装する ソフトウェア

ソフトウェアの開発現場では仕様で決められた処理フローやロジックを実装に落とし込んでいくという作業が多くを占めますが、複雑な要求の部分は不具合の原因になりがちです。

どのように実装すればミスが起こりづらいでしょうか。

基本的な考え方:仕様書そのままの形で実装する

例えば「0~9の10個の整数のうち、偶数のものを標準出力する」という要求がある時、普通に考えれば以下のように実装します。

Python
for i in range(10):
    if i % 2 == 0:
        print(i)

一方「奇数は表示しない」と考えれば、以下のようにも実装できます。

Python
for i in range(10):
    if i % 2 != 0:
        continue
    print(i)

ではどちらが良いコードでしょうか?

私は要求を素直に実装した前者だと思いますし、このような簡単なケースでは普通前者で実装すると思います。要求に偶数と書いてある以上、奇数を示す論理式が表れるのは分かりやすいとは言えません。

しかしながら、要求が複雑化してきたり、コードの改変を繰り返すうちに意図せず後者のような「要求仕様と同値と思われる」実装に変わっていくことがあります。そして多くの場合そこにバグが潜みます。同値の論理を別の形で書くというのは意外と難しいです。

そのような事態を防ぐには、以下のようなことを意識すると良いと思います。

  • 論理は要求の形そのままで使う(反転や、同値と思われる変形をなるべく避ける)
  • コードと仕様の1:1対応が見て取れるような実装にする(仕様書で言及されている単位で関数を分割する、シンボル名を仕様書で使われている言葉と同じ命名にするなど)

私は例え冗長に見えたとしても、まずは要求仕様をそのまま書き下すような形で実装を始めるのが良いと考えています。その方が不具合を見つけても修正が容易ですし、仕様変更にも対応しやすくなります。

発展的な考え方:要求をテスト側で表現する

しかしながら、実直な実装を行って全てがうまくいくわけではありません。

コードの見通しの問題、パフォーマンスの問題等で、コードの表現を変えた方が良い場面も当然あります。例えば早期returnや早期continueなどと呼ばれる、論理を判定して条件分岐のネストを浅くするテクニックがあります。

先ほどの要求「0~9の10個の整数のうち、偶数のものを標準出力する」に加えて「ただし4以外」という条件が加わったとします。普通に考えると以下のように修正されるでしょう。

Python
for i in range(10):
    if i % 2 == 0:
        if i != 4:  # この条件を追加
            print(i)  

ネストが一段階深くなりました。この程度であれば許容だとは思いますが、実際の開発現場では深く入り組み見通しが悪くなった条件分岐を目にすることは多いです。

そこで、条件を反転してcontinueを書くことでネストを深くせずに済みます。関数でreturn文を使う時にも使えるテクニックです。

Python
for i in range(10):
    if i % 2 != 0:  # 条件を反転
        continue
    if i == 4:  # 条件を反転
        continue
    print(i)  # printのネストが浅くなっている

ただし仕様書の条件と実装が逆転してしまうため、仕様書とコードの対応関係は少々分かりづらくなってしまいます。複雑になってくればバグを混入してしまうかもしれません。

このような場合にはロジック側で仕様書を表現するのではなく、テストコード側で仕様書を表現するようにします。

標準出力ではテストしにくいため、まず処理を関数化してリストを返すようにします。そのようにしてテスト対象となるコードを分離(関数化)したのち、リストの中身が正しいかをテストするようにします。

Python
# 仕様書そのままの論理ではないが、ネストを改善したロジック
def get_number_list():
    number_list = []
    for i in range(10):
        if i % 2 != 0:
            continue
        if i == 4:
            continue
        number_list.append(i)
    return number_list

# 簡易的なテストコード
# こちら側で仕様が表現されている
number_list = get_number_list()
assert number_list == [0, 2, 6, 8]

assertの1行で要求仕様が明確に表現されていることが分かります。

※単純な例なのでassert文を使いましたが、Pythonであればunittestモジュール等、単体テストの仕組みを使った方が良いです。

まとめ

基本的にはプロダクトコードかテストコードのどちらかで、なるべく仕様書そのままの形でロジックが表現されているのが望ましいと思います。問題が発生した時に該当のコードと1:1で対応を確認しながら仕様に即しているのかを確認できるためです。

現代的なソフト開発の基本ではテストコードは必ず書くべきですし、テストコードが「動かせる仕様書」と言われることもあるように、やはりテストコード側で仕様書を表現し、安全にリファクタリングや高速化を進めるのが良いと思います。

セオリーとしては開発初期は単純な形で実装してテストを通過できるようにし、仕様が固まってきた段階でテストで保護された状態でリファクタリングを行います。

しかしながら実際の現場ではテストの工数が用意されていなかったり、既にあるコードが複雑化しすぎてテストの導入が困難なこともあります。そのような場合にはまずプロダクトコードの側で仕様書を表現するよう実装してみるという手法も有効であるように思います。

コメント

タイトルとURLをコピーしました