バグ・パターン

Javaプログラムで頻発しがちなバグの分析と修正

今回から「Javaコードの診断」という新しいコラムが始まります。この隔週のコラムでは、日々のプログラミング作業に役立つJavaソリューションを取り上げていきます。今回の入門編ともいうべき記事では、バグ・パターンという概念について紹介します。このきわめて有用な概念をしっかりと把握すれば、コードに潜むバグを検出して修正する能力が向上します。まずは、非常に一般的なバグ・パターンを1つ取り上げましょう。そのパターンを土台にして、さらに複雑なパターンを識別し、そのようなバグを回避するための方法を学んでいきます。

Eric Allen (eallen@cs.rice.edu), Ph.D. candidate, Java programming languages team, Rice University

Eric Allen氏は、コーネル大学でコンピューター・サイエンスおよび数学を専攻し、A.B.(Bachelor of Arts)を取得しました、また現在は、ライス大学、Javaプログラミング言語チームの博士課程に在籍しています。彼の研究対象は、Java言語のソース / バイトコード・レベルでのセマンティック・モデルおよびスタティック分析ツールの開発です。また現在は、NextGenプログラミング言語 (ジェネリック・ランタイム・タイプを持つJavaの拡張言語) のためのソース・ツー・バイトコード・コンパイラーを作成しています。彼の連絡先は、eallen@cs.rice.edu です。



2001年 2月 01日

バグ・パターン:なぜ有用な概念と言えるか

プログラミングのスキルを向上させるには、デザインパターンを数多く知っておくことが必要です。数多く知っておけば、そのようなパターンを状況に応じて組み合わせたり応用したりできます。それと同じように、デバッグのスキルを向上させるためには、バグ・パターン について知っておく必要があります。バグ・パターンとは、エラー現象とその原因となったプログラム中のバグの、繰り返し見られる相関関係を指します。これは、プログラミングの世界で新しい概念であるというわけではありません。また、医師も病気の診断をするときに、同じような相関関係に注目します。医師の場合は、インターン時代に先輩の医師と一緒に働きながら、そのような診断方法を学んでいきます。つまり、医学教育は、診断の方法を中心としているわけです。ところが、ソフトウェア・エンジニアに対する教育では、設計プロセスとアルゴリズム分析が中心になっています。もちろん、そのようなスキルも重要ですが、その一方で、デバッグのプロセスについてはあまり教えられていません。デバッグのスキルは、自分で身につけていくというのが実情です。エクストリーム・プログラミングが登場し、単体テストに重きが置かれるようになり、そのような状況も変わりつつありますが、それでも、頻繁な単体テストで解決できるのは、問題の一部に過ぎません。バグが見つかれば、バグを診断して、修正しなければなりません。幸いなことに、多くのバグはいくつかのパターンに類別できます。そのようなパターンを識別できれば、バグの原因分析と修正をさらにスピードアップできます。

バグ・パターンは、アンチパターンと関係があります。アンチパターンというのは、失敗することが幾度も実証されているソフトウェア設計に共通するパターンのことを指します。ただし、アンチパターンが、設計のパターンであるのに対し、バグ・パターンは、プログラミングのミスから発生するプログラムのエラー動作のパターンです。ここでのポイントは設計ではなく、プログラミングとデバッグのプロセスなわけです。


実例で学ぶ

バグ・パターンの背後にある概念を実際の例で見てみましょう。ここで取り上げるのは、ごく基本的なバグ・パターンです。もちろん、プログラミングの初心者が頻繁に陥るパターンではありますが、上級者にとっても決してあなどれないパターンです。今後の記事では、さらに複雑なバグ・パターンを取り上げる予定です。それぞれのパターンについては、そのパターンの発生を最小限に抑えるためのプログラミング上の原則も示しましょう(そうはいっても、プログラミング上の原則を守ってさえいれば、絶対にバグが発生しないという意味ではありません。どれだけ原則を守っていたとしても、間違いというものはだれにでもあります)。

これから、バグ・パターンについて説明していきますが、便宜上、次のような形式で各パターンについてまとめることにします(用語については、医学の世界からいくらか拝借しました)。

  • パターン名
  • 症状
  • 原因
  • 治療法と予防策

「不良タイル」パターン

プログラミングの初心者の間に最も頻繁に見られるバグ・パターンは、プログラム内のコード・ブロックを別の場所にコピー& ペーストすることから発生するようです。そのような場合に、若干の機能要件の違いから、コピーした部分を一部だけ変更することがあります。そのようにした場合は、1つのコピー箇所でバグを修正しても、別のコピー箇所ではバグがそのままになっているということがどうしても起こり得ます。プログラマーとしては、エラーが繰り返し発生するのを見て、頭をかくことになるわけです。ほとんどのプログラマーは、このパターンのバグにすぐに気づきますが、その発生を最小限に抑えるための対策を講じる人はまずいません。考えるのが面倒だから、大丈夫だと思うコードをコピーしようという気持ちになるのも、わからないわけではありません。しかし、やたらにコピー & ペーストをやって、バグの修正に手間取るようであれば、せっかくコピーによって生産性を上げたつもりでも、その効果はほとんどなくなってしまいます。

このようなパターンのことを「不良タイル」パターンと呼ぶことにしましょう。コード・ブロックをあちこちにコピーした状態は、プログラムを「タイル」で覆ったような状態に似ているからです。いろいろなコピー箇所に変更を加えたときに、「不良タイル」ができる危険があります。

症状

このパターンのバグの最も一般的な症状は、問題を修正したと思っても、プログラムのエラー動作が続くということです。

原因

原因を理解するために、次のようなバイナリー・ツリーのクラス階層を取り上げてみましょう。

public abstract class Tree {
}
public class Leaf extends Tree {
 public Object value;
 ...
}
public class Branch extends Tree {
 public Object value;
 public Tree left;
 public Tree right;
 ...
}

上記のクラスでまず注目したいのは、両方の具体クラスに、Object 型のvalue フィールドがあるということです。後日、このツリーに、たとえばInteger などを含めることにした場合、どちらかのフィールド宣言を変更し忘れることがあるかもしれません。プログラム内のどこかの箇所で、これらのフィールドがInteger であることを前提としているならば、このプログラムはおそらくコンパイルできません。どちらかのクラスのvalue フィールドの型を変更したことは覚えていても、もう片方のクラスについてはそのままにしてあることを忘れてしまう可能性があるわけです。

予防策

もちろん、この例の場合は、プログラミングの初心者でも、共通コードをくくり出すことによって簡単に修正できます。つまり、フィールド宣言をTree クラスに移動するわけです。両方のサブクラスは、そのフィールド宣言を継承するようになるので、フィールド宣言の変更は1箇所だけですみます。

この例を使って、Tree 内の全ノードの加算メソッドと乗算メソッドを記述してみましょう。わかりやすくするために、これらのメソッドを再帰的に記述することにします。

 // in class Tree:
 public abstract int add();
 public abstract int multiply();
 // in class Branch:
 public int add() {
  return this.value.intValue() + left.add() + right.add();
 }
 public int multiply() {
  return this.value.intValue() * left.multiply() + right.multiply();
 }
 // in class Leaf:
 public int add() { return this.value.intValue(); }
 public int multiply() { return this.value.intValue(); }

ここで、Branch クラスのmultiply メソッドに入れておいたバグに注目してください。第2項と第3項の間には乗算記号が入るべきですが、加算記号が入っています。このエラーが発生したのは、multiply メソッドを記述するときに、add メソッドからコードをコピーして若干の変更を加えたものの、その変更が中途半端になっているからです。このバグは実に厄介で、multiply メソッドを呼び出しても、エラーは通知されません。ほとんどの場合、いかにもそれらしい結果が戻されるはずです。

前の場合と同じように、この種のバグを最小限に抑えるには、共通コードをくくり出します。つまり、Tree の前に演算子 (引数として渡される) を配置するメソッドを1つ記述するわけです。この演算子を1つのオブジェクトの中にカプセル化するために、コマンド・パターンと呼ばれるデザインパターン(バグ・パターンではない !) を使用します。

public abstract class Operator {
 public abstract int apply(int l, int r);
}
public class Adder extends Operator {
 public int apply(int l, int r) {
   return l + r;
 }
}
public class Multiplier extends Operator {
 public int apply(int l, int r) {
   return l * r;
 }
}

このようしておけば、Tree クラス階層内のメソッドを次のように変更できます。

 // in class Tree:
 public abstract int accumulate(Operator o);
 public int add() {
   return this.accumulate(new Adder());
 }
 public int multiply() {
   return this.accumulate(new Multiplier());
 }
 // in  class Leaf:
 public int accumulate(Operator o) {
   return value.intValue();
 }

in class Branch: public int accumulate(Operator o) {
   return o.apply(this.value.intValue(),
                  o.apply(left.accumulate(o),
                         right.accumulate(o)));
 }

このように共通コードをくくり出しておけば、addmultiply のメソッド本体で、コピー & ペーストによるエラーが発生する危険を解消できます。さらに、add メソッドとmultiply メソッドをTree の各サブクラスで別々に記述する必要もなくなっています。

共通コードをくくり出すことは、確かに良い習慣ではありますが、あらゆる状況に応用できるわけではありません。たとえば、Javaの型システムはたいへんシンプルなので、精密な型検査を取るか、それともプログラム内の各機能要素の一元管理を取るか、といった二者択一を迫られることがよくあります(参考文献で、NextGenに関する筆者の記事を参照)。そのような場合を考えると、「不良タイル」パターンは、やはり開発者として最小限に抑えるように常に努力しなければならないバグ・パターンであると言えます。


次回の予告

ここで、最初のバグ・パターンを要約しておきます。この部分を切り取って専用の掲示板にでも貼り付けておけば、何かのときに参照できるでしょう。

  • パターン名: 不良タイル
  • 症状: バグを修正したはずなのに、いつまでもエラーが続くような状況。
  • 原因: コードをあちこちにコピー & ペーストした場合に、少なくとも1箇所はバグが残っている。
  • 治療法と予防策: 可能なら共通コードをくくり出すこと。あるいは、変更を試みること。コードのコピー& ペーストをやめること。

次回の記事では、Javaコードで発生する一般的なバグ・パターンをさらにいくつか取り上げます。特に、ヌル・ポインター例外として発生するバグ・パターンを示し、そのパターンの発生を最小限に抑えるための方法を説明する予定です。

参考文献

  • アンチパターンのホーム・ページには、アンチパターンに関する情報や関連資料のリンクがあります。
  • エクストリーム・プログラミングについてお調べください。これは、バグのない堅固なソフトウェアを短時間で開発するための新しい手法です。
  • JUnit をダウンロードして、単体テストをすぐに始めてみてください。
  • NextGen (ランタイム汎用型を用意したJava拡張言語) に関する筆者の記事では、共通コードをくくり出すか、それとも型システムを使用してコンパイル時にエラーを検出するか、という二者択一の問題を、さらに強力な型システムによっていくらかでも解消する方法を説明しています。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219623
ArticleTitle=バグ・パターン
publish-date=02012001