なんでもかんでも「バグ」ってひとくくりにしないで

はじめに

プログラマがソフトウェアを作るとユーザがつきます。ユーザがそのソフトウェアを使っていて何らかの問題が発生すると「このソフトはバグってる、直して!」と言われることがままあります。それに対して「いや、仕様だから」と突っぱねられることがあります。その後お互いの意見が「バグだ!」「いいや仕様だ!」と平行線になってお互いモヤモヤのまま終わるというのはよくある話です。

なぜこういうことが起きるかというと、原因の一つは「問題」イコール「バグ」という短絡的な考え方です。とくにソフトウェアを作ったり使ったりした経験が浅い人がこうなる傾向があると推測しています。このような食い違いは「要件」「仕様」と「実装」という言葉の意味を理解していればある程度解決できます。本書はこれらの用語について実例を挙げて簡単に紹介します。

注意点

  • 本記事では要件や仕様を定義することが前提となっていますが、とくにユーザと開発者が同じような場合にはこれらが明確でないことがままあります。OSSにおけるこのあたりの感覚についてはこの記事が非常に参考になります。興味のあるかたはごらんください
  • 原因がわかったときにその責任がどこにあるかについて。たとえば原因がソフトウェアの誤りであることがわかっても、たとえばGPLのように「無保証」と銘打っているライセンスを採用しているソフトウェアについては「ここバグってるからソフトウェアを直して」とユーザが言っても開発者には直す責任はありません(運が良ければ直してくれることもあります)。この場合は自分で修正を作って開発者に提供して直してもらうなどの対処が考えられます。このあたりは本記事のスコープから外れますのでこれ以上は述べません。今後「開発者に責任がある」と書いている場合は、無保証のソフトウェアについては状況が違うと考えてください。
  • 本記事に書いてあるようなことは百も承知で、自分あるいは自分の所属する組織に有利になるようにゴネる人もいますが、そういうケースはここでは扱いません。同じく、責任が誰にあるかは明確であってもユーザと開発者の力関係によって誰に責任があるのかが政治的に決まる、場合によっては両者の意見の相違によって裁判沙汰になることもありますが、それについてもここでは扱いません。

要件

ソフトウェアを作る際には、最初に「いったどういうソフトウェアを作りたいのか」という要件を定義します。ここでは次のような要件があるとします。

  • ある組織に複数の人が属している
  • 個々人について名前と年齢という情報が並べられた表を管理したい
  • 年齢をキーとして組織に属する人すべての情報を昇順に表示したい

この要件定義はユーザ(自分自身かもしれません)の思いを反映しているものとします1

仕様

仕様とはソフトウェアのあるべき姿です。ここでは個々人の情報を以下のstruct personという構造体を使って扱うものとします。

struct person {
  char *name;
  int age;
}

ここでageは必ず0以上の数値にしなければならないとします。

ここからはstruct personという構造体の配列をソートするsort()という関数の仕様を次のように定義したとします。

  • インターフェースはint sort(struct *person[] a, int len)
  • 配列aageフィールドに基づいて昇順にソートする。ソートされた新しい配列を作るのではなくaの中身をそのままソートする。呼び出しが成功した場合は呼び出し後のaはa[0]->age <= a[1]->age <= ... <= a[len - 1]->ageとなる
  • 引数の意味
  • a: ソート対象の配列
  • len: aの長さ。1以上
  • 戻り値
  • 0: 成功
  • 1: 失敗。age0未満のときやlen1未満のとき

仕様の誤り

ここでsort()関数の仕様が前節において述べたものではなく次のようなものだったとします。もとの仕様と違うところは太字で書いています。

  • インターフェースはint sort(struct *person[] a, int len)
  • 配列aageフィールドに基づいて降順にソートする。ソートされた新しい配列を作るのではなくaの中身をそのままソートする。呼び出しが成功した場合は呼び出し後のaa[0]->age >= a[1]->age >= ... >= a[len - 1]->ageとなる
  • 引数の意味
  • a: ソート対象の配列
  • len: aの長さ。1以上。len0未満の場合の動作は未定義
  • 戻り値
  • 0: 成功
  • 1: 失敗。age0未満のときやlen1未満のとき

要件によるとsort()関数は配列aを昇順にソートすべきでしたが、ここでは降順にソートしてしまっています。このように要件に沿っていない場合は開発者が作った仕様が誤っているという問題が発生したといえます。誰に責任があるかは契約次第なのですが、ユーザが仕様にOKを出して全責任を負う、という契約であればユーザの責任ですし、開発者が仕様を自由に決めてよくて、かつ、開発者が責任を負うという契約の場合は開発者の責任となります。

仕様の誤りには他にも次のようなものが考えられます。

  • インターフェースはint sort(struct *person[] a, int len)
  • 配列aageフィールドに基づいて降順にソートする。ソートされた新しい配列を作るのではなくaの中身をそのままソートする。呼び出しが成功した場合は呼び出し後のa[]a[0]->age < a[1]->age < ... < a[len - 1]->ageとなる
  • 引数の意味
  • a: ソート対象の配列
  • len: aの長さ。1以上。len0未満の場合の動作は未定義
  • 戻り値
  • 0: 成功
  • 1: 失敗。age0未満のときやlen1未満のとき

こちらは個々人の年齢が同じ場合に仕様を満たせないため、これも仕様が誤っていると言えます。

実装

ではsort()関数の実装について考えます。ここでいう実装とはsort()関数を仕様に定められた通りに動くようにコード化することです。仕様を満たしさえすればどういう実装にしてもかまいません。なお、ここでは実装のコードそのものは重要ではないので考えません。

実装の誤り

sort()関数の実装が、a[]を降順にソートしてしまうものだった場合はどう考えればいいでしょうか。これは実装が仕様を満たしていないので、開発者による実装の誤りが発生した問題と言えます。責任の所在については、たとえばユーザが仕様にOKを出して全責任を負う、という契約であればユーザの責任ですし、開発者が仕様を自由に決めてよくて、かつ、開発者が責任を負うという契約の場合は開発者の責任となります。

使用方法の誤り

sort()関数の仕様も実装も正しいとします。ではこれで問題が発生しないかというとそんなことはありません。たとえばユーザがデータ入力を間違えており、a[]の中にageが-10の要素があったとします。この場合、sort()関数はエラーを示す1を返します。ここでユーザが「なぜソートされていないんだ、これは問題だ!バグだ!」ということはよくあるのですが、これはsort()関数自体は仕様通りに1を返しているだけなのでユーザが使用方法を誤っているといえます。

実装依存のふるまい

sort()関数の実装に使われるソートアルゴリズムには安定ソートと不安定ソートという二種類のものがあります。これらの意味については本記事のスコープを外れるのでwikipediaの記事などをご覧ください。sort()関数の仕様を見ると、この関数が安定ソートなのかどうかということは定められていません。このような場合に実装においてどちらの種類のアルゴリズムにするかは開発者にゆだねられています。

ここでユーザが「ソートといえば安定ソートだろ」という想定でこのソフトウエアを使って思い通りにならなかった場合に「これは問題だ!バグだ!」ということがありますが、これについては安定ソートか否かは仕様によって未定義なので責任は通常ユーザにあります。

バージョンアップと実装依存

たまたまsort()関数が安定ソートであり、かつ、ソフトウェアのバージョンアップに伴いsort()関数の実装が変わって不安定ソートにする場合はどのように扱えばよいでしょうか。これについては考え方が色々ありますが、原理原則からいうと未定義の動作がどうなろうとこれは問題ありません。ただし特別な理由がない限りこういうことは避けるほうがよいでしょう。文化によってはこの手の変更が決して許されないこともあります。

おわりに

ここまでみてくださったかたがたに感謝いたします。これを読み切ったかたの多くは何らかの形で本記事冒頭で述べたような状況に遭遇したことがあるのではないかと思います。それが作る側であったとしても使う側であったとしても、言葉の定義があやふやなことによる不幸な話の食い違いがこの記事によって多少なりとも減ることを祈ります。


  1. とくにユーザと開発者が違う人の場合に要件定義がそもそも間違っていることも多々ありますが、それについては割愛します