プログラミングのスキルを向上させるには、デザインパターンを数多く知っておくことが必要です。数多く知っておけば、そのようなパターンを状況に応じて組み合わせたり応用したりできます。それと同じように、デバッグのスキルを向上させるためには、バグ・パターン について知っておく必要があります。バグ・パターンとは、エラー現象とその原因となったプログラム中のバグの、繰り返し見られる相関関係を指します。これは、プログラミングの世界で新しい概念であるというわけではありません。また、医師も病気の診断をするときに、同じような相関関係に注目します。医師の場合は、インターン時代に先輩の医師と一緒に働きながら、そのような診断方法を学んでいきます。つまり、医学教育は、診断の方法を中心としているわけです。ところが、ソフトウェア・エンジニアに対する教育では、設計プロセスとアルゴリズム分析が中心になっています。もちろん、そのようなスキルも重要ですが、その一方で、デバッグのプロセスについてはあまり教えられていません。デバッグのスキルは、自分で身につけていくというのが実情です。エクストリーム・プログラミングが登場し、単体テストに重きが置かれるようになり、そのような状況も変わりつつありますが、それでも、頻繁な単体テストで解決できるのは、問題の一部に過ぎません。バグが見つかれば、バグを診断して、修正しなければなりません。幸いなことに、多くのバグはいくつかのパターンに類別できます。そのようなパターンを識別できれば、バグの原因分析と修正をさらにスピードアップできます。
バグ・パターンは、アンチパターンと関係があります。アンチパターンというのは、失敗することが幾度も実証されているソフトウェア設計に共通するパターンのことを指します。ただし、アンチパターンが、設計のパターンであるのに対し、バグ・パターンは、プログラミングのミスから発生するプログラムのエラー動作のパターンです。ここでのポイントは設計ではなく、プログラミングとデバッグのプロセスなわけです。
バグ・パターンの背後にある概念を実際の例で見てみましょう。ここで取り上げるのは、ごく基本的なバグ・パターンです。もちろん、プログラミングの初心者が頻繁に陥るパターンではありますが、上級者にとっても決してあなどれないパターンです。今後の記事では、さらに複雑なバグ・パターンを取り上げる予定です。それぞれのパターンについては、そのパターンの発生を最小限に抑えるためのプログラミング上の原則も示しましょう(そうはいっても、プログラミング上の原則を守ってさえいれば、絶対にバグが発生しないという意味ではありません。どれだけ原則を守っていたとしても、間違いというものはだれにでもあります)。
これから、バグ・パターンについて説明していきますが、便宜上、次のような形式で各パターンについてまとめることにします(用語については、医学の世界からいくらか拝借しました)。
- パターン名
- 症状
- 原因
- 治療法と予防策
プログラミングの初心者の間に最も頻繁に見られるバグ・パターンは、プログラム内のコード・ブロックを別の場所にコピー& ペーストすることから発生するようです。そのような場合に、若干の機能要件の違いから、コピーした部分を一部だけ変更することがあります。そのようにした場合は、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)));
}
|
このように共通コードをくくり出しておけば、add とmultiply のメソッド本体で、コピー & ペーストによるエラーが発生する危険を解消できます。さらに、add メソッドとmultiply メソッドをTree の各サブクラスで別々に記述する必要もなくなっています。
共通コードをくくり出すことは、確かに良い習慣ではありますが、あらゆる状況に応用できるわけではありません。たとえば、Javaの型システムはたいへんシンプルなので、精密な型検査を取るか、それともプログラム内の各機能要素の一元管理を取るか、といった二者択一を迫られることがよくあります(参考文献で、NextGenに関する筆者の記事を参照)。そのような場合を考えると、「不良タイル」パターンは、やはり開発者として最小限に抑えるように常に努力しなければならないバグ・パターンであると言えます。
ここで、最初のバグ・パターンを要約しておきます。この部分を切り取って専用の掲示板にでも貼り付けておけば、何かのときに参照できるでしょう。
- パターン名: 不良タイル
- 症状: バグを修正したはずなのに、いつまでもエラーが続くような状況。
- 原因: コードをあちこちにコピー & ペーストした場合に、少なくとも1箇所はバグが残っている。
- 治療法と予防策: 可能なら共通コードをくくり出すこと。あるいは、変更を試みること。コードのコピー& ペーストをやめること。
次回の記事では、Javaコードで発生する一般的なバグ・パターンをさらにいくつか取り上げます。特に、ヌル・ポインター例外として発生するバグ・パターンを示し、そのパターンの発生を最小限に抑えるための方法を説明する予定です。
-
アンチパターンのホーム・ページには、アンチパターンに関する情報や関連資料のリンクがあります。
-
エクストリーム・プログラミングについてお調べください。これは、バグのない堅固なソフトウェアを短時間で開発するための新しい手法です。
-
JUnit をダウンロードして、単体テストをすぐに始めてみてください。
-
NextGen (ランタイム汎用型を用意したJava拡張言語) に関する筆者の記事では、共通コードをくくり出すか、それとも型システムを使用してコンパイル時にエラーを検出するか、という二者択一の問題を、さらに強力な型システムによっていくらかでも解消する方法を説明しています。
Eric Allen氏は、コーネル大学でコンピューター・サイエンスおよび数学を専攻し、A.B.(Bachelor of Arts)を取得しました、また現在は、ライス大学、Javaプログラミング言語チームの博士課程に在籍しています。彼の研究対象は、Java言語のソース / バイトコード・レベルでのセマンティック・モデルおよびスタティック分析ツールの開発です。また現在は、NextGenプログラミング言語 (ジェネリック・ランタイム・タイプを持つJavaの拡張言語) のためのソース・ツー・バイトコード・コンパイラーを作成しています。彼の連絡先は、eallen@cs.rice.edu です。