BOOK Preview

Code Complete 第2版 上・下
― 完全なプログラミングを目指して

第24章 リファクタリング

マイクロソフトプレスの書籍紹介ページ
書籍情報のページ
2005/05/10


■ case文を並行して修正しなければならない
 case文自体は悪くないが、同じようなcase文をプログラムの何か所かで変更していることに気付いたら、継承を使った方がよいかどうかを検討する。

■ 一緒に使用する関連データがクラスにまとめられていない
 同じデータのセットを繰り返し処理していることに気付いたら、それらの処理を専用のクラスとして独立させるべきかどうかを検討する。

■ ルーチンがそのクラスの機能よりも別のクラスの機能を多く使用する
 ルーチンを別のクラスに移動して、元のクラスから呼び出すようにすべきだろう。

■ 基本データ型をオーバーロードしている
 基本データ型では、現実世界の無数のエンティティを表すことができる。プログラムがお金といった一般的なエンティティを表すために整数などの基本データ型をオーバーロード(多重定義)して使用している場合は、簡単なMoneyクラスを作成して、Moneyクラスの変数の型をコンパイル時にチェックしたり、代入された値の検証などを追加したりできるようにする。Money(お金)とTemperature(温度)がどちらも整数である場合、bankBalance = recordLowTemperatureのような不正な代入を行ったとしても、コンパイルエラーにはならない。

■ クラスがほとんど何もしない
 コードをリファクタリングした結果、古いクラスの仕事がほとんどなくなることがある。クラスがそれ相応の役割を果たしていないようであれば、そのクラスのすべての役割を他のクラスに割り当てて、クラスをなくしてしまうべきかどうかを検討する。

■ ルーチン間を「トランプデータ」が流れる
 ルーチンから別のルーチンにデータを渡すだけの目的で、そのルーチンにデータを渡すことを「トランプ(tramp:放浪)データ」と呼ぶ(Page-Jones 1988)。それ自体に問題はないとしても、特定のデータを渡すことが各ルーチンのインターフェイスの抽象化と一貫しているかどうかを検討する。各ルーチンのインターフェイスの抽象化に問題がなければ、データを渡しても問題はない。抽象化に問題がある場合は、各ルーチンのインターフェイスに一貫性を持たせる方法について検討する。

■ 中間オブジェクトが何もしていない
 クラスのコードのほとんどが呼び出しを他のクラスのルーチンに渡しているだけだとしたら、中間オブジェクトを削除して、それらのクラスを直接呼び出すべきかどうかを検討する。

■ あるクラスが別のクラスと密結合している
 プログラムを頭で理解しやすいものにし、コードの変更の波及効果を最小限に抑えるとしたら、おそらくカプセル化(情報隠ぺい)が最も強力である。必要以上に他のクラスのことを知っているクラスを見つけたら(サブクラスが親であるスーパークラスのことを知りすぎているなど)、カプセル化を少し強めた方がよい。

■ ルーチンの名前が不適切である
 ルーチンの名前が不適切である場合は、ルーチンが定義されている場所とルーチンが呼び出されているすべての場所で名前を変更し、再コンパイルする。今これを行うことはたいへんかもしれないが、後になればなるほど難しくなるので、問題に気付いたらすぐに変更しよう。

■ データメンバがパブリックである
 私が思うに、パブリックなデータメンバは常に良くない。それらはインターフェイスと実装との境界線をあいまいにし、本質的にカプセル化に違反し、将来の柔軟性を阻止する。パブリックなデータメンバはアクセスルーチンで隠ぺいすることを積極的に検討しよう。

■ サブクラスがスーパークラスのルーチンをほんの一部しか使用しない
 これは一般に、そのサブクラスはスーパークラス(親クラス)に必要なルーチンがたまたま含まれていたために作成されたものであり、サブクラスがスーパークラスの必然的な子孫ではないことを示す。サブクラスとスーパークラスの関係を「is a」関係から「has a」関係に切り替えて、カプセル化を改善することを検討しよう。つまり、スーパークラスを元のサブクラスのデータメンバに書き換え、元のサブクラスで本当に必要なルーチンだけを公開する。

■ 複雑なコードを説明するためにコメントが使われている
 コメントには重要な役割があるが、悪いコードを説明するための埋め合わせとして使用すべきではない。昔から言われているように、「悪いコードを説明するな、書き直せ」である(Kernighan and Plauger 1978)。

■ グローバル変数を使用している
 グローバル変数を使用する部分のコードを見直すときは、時間をかけてもう一度よく調べてみる。最後にその部分のコードに取り組んだ後、グローバル変数を使用しない方法を思い付いているかもしれない。最初に書いたときほどコードをよく覚えていないので、グローバル変数が紛らわしい存在に見えて、もう少しすっきりしたコードにしたいと感じるかもしれない。また、グローバル変数をアクセスルーチンとして分離する方法に以前よりも詳しくなっていて、そうしないと痛い目に遭うことをよく心得ているかもしれない。歯を食いしばって、有意義な変更を加えよう。最初にコーディングしてからかなり時間がたっているため、自分のコードを客観的に見つめることができる。しかし、改良を正しく行うために必要なことを忘れてしまうほどの時間はたっていない。コードを改良するのに最適な時は、早期の見直し段階である。

参照
グローバル変数を使用するためのガイドラインについては、上巻第13章の「13.3 グローバルデータ」を参照。グローバルデータとクラスデータの違いについては、上巻第5章5.3.5の「クラスデータとグローバルデータの取り違え」の項を参照。

■ ルーチンの呼び出し前に初期化コードが使用され、呼び出し後に後処理コードが使用されている
 次のようなコードには注意が必要だ。

この初期化コードに注意
WithdrawalTransaction withdrawal;
withdrawal.SetCustomerId( customerId );
withdrawal.SetBalance( balance );
withdrawal.SetWithdrawalAmount( withdrawalAmount );
withdrawal.SetWithdrawalDate( withdrawalDate );

ProcessWithdrawal( withdrawal );

この後処理コードにも 注意
customerId = withdrawal.GetCustomerId();
balance = withdrawal.GetBalance();
withdrawalAmount = withdrawal.GetWithdrawalAmount();
withdrawalDate = withdrawal.GetWithdrawalDate();
リスト24-1 ルーチン呼び出しのための初期化コードと後処理コードを持つ悪いコードの例(C++)

 同様に、標準的な初期化データをとるWithdrawalTransactionクラスの特別なコンストラクタを作成して、次のようなコードを書けるようにしている場合も要注意である。

withdrawal = new WithdrawalTransaction( customerId, balance,
  withdrawalAmount, withdrawalDate );
withdrawal.ProcessWithdrawal();
delete withdrawal;
リスト24-2 メソッド呼び出しのための初期化コードと後処理コードを持つ悪いコードの例(C++)

 ルーチンを呼び出す前に準備をするコード、あるいはルーチンを呼び出した後に後処理をするコードを見つけたら、必ずルーチンのインターフェイスが正しく抽象化されているかどうかを検討する。この場合は、次のようなコードをサポートするように、ProcessWithdrawal()ルーチンの引数リストを修正すべきである。

ProcessWithdrawal( customerId, balance, withdrawalAmount,
                   withdrawalDate );
リスト24-3 初期化コードや後処理コードを必要としない良いコードの例(C++)

 この逆の場合も同様の問題を抱えていることに注意しよう。通常はWithdrawalTransactionオブジェクトを使用しているが、その値のいくつかを上記のようなルーチンに渡す必要があることに気付いた場合は、ProcessWithdrawal()ルーチンのインターフェイスをリファクタリングして、個々のフィールドではなくWithdrawalTransactionオブジェクトを要求することについて検討しよう。

ProcessWithdrawal( withdrawal.GetCustomerId(),
  withdrawal.GetBalance(), withdrawal.GetWithdrawalAmount(),
  withdrawal.GetWithdrawalDate() );
リスト24-4 複数のメソッド呼び出しを要求するコードの例(C++)

 これらの方法は、どれも状況次第で良くも悪くもなる。その良し悪しは、ProcessWithdrawal()ルーチンのインターフェイスの抽象化によって、別個のデータが4つ必要になるのか、それともWithdrawalTransactionオブジェクトが必要になるのかによって決まる。

■ プログラムに先のことを考えたコードが含まれている
 プログラマはいつか必要になるかもしれない機能を推測するのがへたなことで有名だ。「先のことを考えた設計」を行うと、いろいろな問題が浮上するのは容易に想像できる。

  • 「先のことを考えて設計された」コードの要求は、完全には検討されていない。つまり、プログラマが将来の要求について誤った推測をしている可能性が高い。「先のことを考えたコーディング」が、結局は無駄になってしまう。

  • プログラマが将来の要求をかなり正確に予測していたとしても、一般に要求の子細まですべて予測してはいない。これらの子細によって、プログラマが考えていた基本設計が崩れてしまい、「先のことを考えた設計」を捨てなければならなくなる。

  • 「先のことを考えて設計された」コードを将来使用するプログラマは、それが「先のことを考えて設計された」コードであることを知らないか、コードが実際よりもうまく動くと思い込む。そのコードが他のコードと同じレベルで作成され、テストされ、レビューされていると考える。「先のことを考えて設計された」コードを使用するためのコードの作成に多くの時間を無駄にしたあげく、「先のことを考えて設計された」コードが実は動かないことを知る。

  • 「先のことを考えて設計された」余分なコードが、余分な複雑さを生み、テストや欠陥の修正が余分に必要となる。そして、それらはプロジェクトの遅れとなって現れる。

 専門家の意見では、将来の要求の準備をするには、思惑的なコードを書かないことが一番である。今必要なコードをできるだけすっきりとわかりやすく書き、そのコードを将来使用するプログラマが、そのコードが何をして何をしないのかを理解し、それに従ってコードを変更できるようにする(Fowler1999; Beck 2000)。

チェックリスト24-1 リファクタリングする理由

コードが重複している。
ルーチンが長すぎる。
ループが長すぎる、またはネストが深すぎる。
クラスのモジュール凝集度が小さい。
クラスのインターフェイスが一貫性のある抽象化を実現していない。
引数リストの引数の数が多すぎる。
変更がクラス内部の他の部分に影響しない。
変更が発生したら複数のクラスを同時に修正しなければならない。
継承の階層を並行して修正しなければならない。
case文を並行して修正しなければならない。
一緒に使用する関連データがクラスにまとめられていない。
ルーチンがそのクラスの機能よりも別のクラスの機能を多く使用する。
基本データ型をオーバーロードしている。
クラスがほとんど何もしない。
ルーチン間をトランプデータが流れる。
中間オブジェクトが何もしていない。
あるクラスが別のクラスと密結合している。
ルーチンの名前が不適切である。
データメンバがパブリックである。
サブクラスがスーパークラスのルーチンをほんの一部しか使用しない。
複雑なコードを説明するためにコメントが使われている。
グローバル変数を使用している。
ルーチンの呼び出し前に初期化コードが使用され、呼び出し後に後処理コードが使用されている。
プログラムに先のことを考えたコードが含まれている。

24.2.2 リファクタリングしない理由

 俗にいう「リファクタリング」は、欠陥の修正、機能の追加、設計の修正といった大まかな意味で使われる。基本的には、何らかのコードの変更を意味する言葉として使われる。この言葉の意味がこのように希薄化しているのは残念なことだ。変更自体は褒められたことではないとしても、小さじ1杯の規律に則った目的のある変更は、保守対象のプログラムの品質を着実に向上させ、ありがちなソフトウェアエントロピーのデススパイラルを防ぐ重要な戦略になり得る。


 INDEX
  Code Complete 第2版 上・下
  第24章 リファクタリング
    1.24.1 ソフトウェアの進化の種類
  2.24.2 リファクタリング概論
    3.24.3 リファクタリングの詳細(1)
    4.24.3 リファクタリングの詳細(2)
    5.24.4 安全なリファクタリング
    6.24.5 リファクタリング戦略
 
インデックス・ページヘ  「BOOK Preview」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間