まくまくいろいろノート
不具合修正に対する向き合い方
2015-05-26

不具合そのものではなく、不具合混入の根本原因を突き止める

ソフトウェアの実装に不具合が見つかった時、正しく動作するように修正するだけでは不十分です。 継続的に設計を改善していくためには、どうしてそのような不具合となるコーディングを行ってしまったのか、その根本的な原因を突き止め、その原因を取り除いていく必要があります。 表面的な不具合の修正を繰り返しているだけでは、不具合を生みやすい構造が改善されないため、ソフトウェアの品質はいつまでたっても上がらず、不具合修正の工数だけが増えていきます。

attitude-to-defect.png

不具合を分析するときは、表面化した不具合(結果)にだけ注目するのではなく、不具合混入の根本原因を探りましょう。 下記は、典型的な不具合のパターンと、それぞれどのような根本原因が潜んでいるかの例です。

パターン1: API の使い方を間違えて不具合が入った

大きなプロジェクトでは、他のメンバが作成した API を使用することが多くなるため、このパターンによる不具合は多く発生します。 実装者が API の使用方法をまったく調べずに使ってしまったということも考えられますが、突き詰めていくと、下記のような原因が見えてきます。

  • 変数名やメソッド名から実際の振る舞いが想像できなかった
  • API ドキュメントの記載があいまいで正しい呼び出し方がわからなかった
  • パラメータの型が汎用的すぎて(int 型など)、想定外の値を渡してしまった

このような原因が考えられる場合は、不具合そのもののコードを修正するだけでなく、根本原因となった既存のコードを改善するようにしましょう。それが本当の品質改善です。

変数やメソッドに誤解を招くような名前が付けられているのであれば、意図が正しく伝わるような名前に変更しましょう。 ドキュメンテーションコメントが不十分であれば、誰が読んでも正しく解釈できるように説明文を変更します。 メソッドにテストコードがなければ追加し、メソッドの振舞いをより明確にします。 パラメータが int や String といったプリミティブ型で定義されている場合は、列挙型に変更することで、誤った使われ方を防ぐことができるでしょう。

パターン2: 不具合を修正したら別の不具合を埋め込んでしまった

いわゆるエンバグというものです。 複雑なメソッドを修正する場合にはエンバグのリスクは付き物です。 エンバグしてしまった原因としては下記のようなことが考えられます。

  • 修正を入れたことにより、あるメソッドの戻り値が想定外の値になっていることに気付かなかった
  • ある定数値を変更したときに、連動して変えなければいけないコードがあることに気付かなかった
  • 修正部分の周囲のコードの意味が分からず、そこに与える影響に気付かなかった

エンバグの特徴としては、不具合の原因がもともとのコードにあることが多いことが挙げられます。 もともとのコードのメンテナンス性が悪ければ、修正時に別の不具合が入ってしまうことは避けられません。 既存のメソッドに副作用が出てしまうことを気付けなかったのは、そのメソッドにテストコードがなかったからです。 テストコードを追加し、間違った修正をしてしまった場合に、その場で気が付けるようにしましょう。

DRY 原則に基けば、同一の値を持つ定数値を、複数個所で管理しなければいけないような設計は改めるべきです。 とはいえ、設計プロセスやアーキテクチャ上の都合で、重複した記述を防げないこともあります(ファイルを分散して置かなければいけないとか)。 そのような場合は、連動して変更しなければいけないコードすべてに、相互のポインタを記述しておくとか、コードを自動生成するなどの仕組みを考えます。

既存コードの設計が複雑で、皆が容易に理解できないようであれば、そのコードは優先的にリファクタリングを進めるべきです。 メソッドが巨大すぎるのであれば分割し、ドキュメンテーションコメントを分かりやすく記述します。 第三者が読んだときにすぐに理解できるようなコードになるまで改善しておかなければいけません。 そうすることで、将来的にそのコードを編集するときにエンバグしてしまうリスクを低減できます。

パターン3: 要求通りの機能になっていなかった

これは実装者がちゃんと要求や仕様を理解できているかの問題になります。 ソフトウェア設計は、ある仕様に基いて進めていくことになりますが、その仕様に対する理解度に差が出てしまう原因はいろいろ考えられます。

  • 仕様書を読んでいない
  • 仕様書を読んで理解したつもりになっていたけれど解釈が間違っていた
  • 仕様書を読んでも理解できないので想像で実装していた
  • 仕様書がない

そもそも仕様書を読まずに実装していたとなると、「ちゃんと読め!」と、頭ごなしに実装者を攻めてしまうことがありますが、まずはなぜ仕様書を読まなかったのかにフォーカスするのがよいでしょう。 仕様書が簡単にアクセスできるところにアップロードされていないのではないか? 仕様書を読み込むための工数が確保されていないのではないか? 仕様書内でローカルな専門用語を使いすぎていて読んでも理解できないのではないか? 仕様書が読みにくいのではないか? などなど、より上位に問題が潜んでいるかもしれません。

仕様書をじっくりと読んだにもかかわらず認識の齟齬が出てしまう場合は、仕様書自体に問題があることが多いです。 仕様書の記載方法に関する研修を受けたり、チーム内で仕様書の記載に関する改善ブレストなどをするとよいでしょう。

仕様が複雑な場合は、コード内に仕様書へのリンクを埋め込んだり、詳しいドキュメンテーションコメントを記述しておかなければいけません。 将来的にそのコードをメンテナンスする人のことを考えれば、それがどれだけ重要なことか分かるはずです。 そのコードを実装するときに特定の資料が必須であったのであれば、その資料がアップロードされているサーバの URL を記載しておきましょう。 また、その複雑な仕様が、ユーザにとっても複雑なものになっているのであれば、仕様自体をシンプルにできないか見直すべきです。

仕様書側に改善できるポイントが見つかった場合は、不具合修正(コード修正)と同じタイミングで仕様書の修正も行ってしまいましょう。 あいまいな記述がなくならない限り、将来にわたって不具合は発生し続けます。 できれば、その仕様に関わる部分のテストコードを記述し、その振舞いが要求を満たしているかを確認しましょう。 テストコードを記述する過程で、仕様定義のあいまいさに気付けることもあります。

仕様書を書かない、もしくは、ごく簡単な仕様記述しか行わないという文化を持つところもあると思います(常に全員が一か所で作業するベンチャー企業など)。 そいうったケースでは、口頭での意思疎通によって仕様を理解することになります。 十分に要求を理解できない状態で実装を進めてしまうようであれば、実装者の仕事に対する向き合い方に問題があります。 仕様に関して質問をしにくい環境なのであれば、その環境を改めるべきです。 要求を十分に理解したと思って実装を進めたにもかかわらず、できあがったものが想定外のものになってしまうということであれば、もともとの仕様が不自然であった可能性があります(少なくともその実装者の中の常識とは異なる仕様になっている)。 仕様自体を考え直すきっかけになるでしょう。

不具合に対する向き合い方と、不具合の管理方法

経験を積んだプログラマは、その場の修正が楽だからという理由だけで不具合の修正方法を決めたりはしません。 不具合の根本原因となっているコードや環境をそのままにしておくと、将来的にも同じような不具合を生み出し、何度も苦労することになることを知っているからです。 誰かが間違えてしまった部分は、他の人も間違える可能性が高いのです(それは将来のあなたかもしれません)。 同じ過ちを繰り返さないように導いてください。

企業内のプロジェクトであれば、何らかの不具合管理ツールを導入しているはずです。 可能であれば、「不具合混入の根本原因」、「今後の不具合混入の防止策」という項目をテンプレート化して、コード品質を改善するための作業を習慣づけましょう。

その場しのぎの不具合修正を繰り返しているとつらいだけです。 分かりにくいコードをそのままにしておくのは精神的にもよくありません。 不具合を見つけたときは、設計を改善するためのよい機会だと思いましょう。 修正箇所にテストコードが存在しないのであれば、テストコードを追加するチャンスです。 上司が普段のリファクタリング作業に反対しているのであれば、不具合修正時がリファクタリングを行うチャンスです。

不具合修正というと大変なイメージがあるかもしれませんが、自分は設計の改善を行っているのだと考えられるようになれば、プログラマとして成長した証です。

2015-05-26