リファクタリング

リファクタリングとは

リファクタリングとは、外部から見たときの振る舞いを保ったまま、コードの理解や修正が容易になるように内部構造を変更することである。

一般的に、システムは規模が大きく複雑になるほど、バグが発生しやすく、バグの発生箇所の特定や修正が難しくなる。ほうっておくと依存関係は複雑に絡み合い、どのように動いているのか理解するだけでも時間がかかり、それに伴って機能の追加する速度もどんどん遅くなる。(規模の不経済)

一方で、リファクタリングによって、内部構造が改善され、適切にモジュール化され、依存関係も整理されていると、変更の際に理解しなければならない箇所は限定され、変更による影響も限定される。これによって、バグを埋め込んでしまう可能性は減少し、バグが発生したとしても修正が簡単になる。その結果、システムの規模が大きくなっても開発速度は遅くならない。

Notion Image

リファクタリングは多くの場合、以下の順序で進める。

  • テストの存在確認 リファクタリングを実施しようとしている部分の振る舞いをチェックするテストが書かれているか確認する。もしテストがなければ、まず現在の振る舞いをチェックするためのテストを作成する。
  • リファクタリング 大きなバグが入り込まないように、小さなリファクタリングを実施する。
  • テストの実行 テストを実行し、振る舞いが変化していないことを確認する。
  • フィードバック リファクタリングを進めていくうちに得られた気づきをもとに、設計を考え直す。もしリファクタリング後に状況が悪化(理解しにくくなる、修正しにくくなるなど)したら、苦渋に満ちた表情を浮かべながらリファクタリング前の状態に戻す。
  • 繰り返す さらにリファクタリングを進める。
  • リファクタリングはそれ専用に時間を確保するようなものではなく、機能の追加やバグの修正の最中にその作業の一部として実施するものである。そのため、マネージャーや顧客がリファクタリングの重要性を理解していないときは、無理に説得する必要はなく、「彼らに黙ってリファクタリング」すればよい。

    リファクタリングとパフォーマンス

    一般的に、リファクタリングによってプログラムが適切にモジュール化されていくほど、プログラムの処理速度は遅くなり、メモリの使用量は多くなる。ただし、近年のコンピュータの性能を考慮すると、このパフォーマンスの問題はほとんど気にする必要がない。

    なお、パフォーマンスの問題は一部分に集中していることが多く、コード全体にわたって最適化を実施しても、90%の努力は無駄になってしまう。そのため、医療機器のような厳しい要求がある場合を除き、まずは処理速度など気にせずにリファクタリングを実施して、プログラムを適切にモジュール化する。その後、時間やメモリを計測してパフォーマンスのボトルネックとなる箇所を特定し、その部分の最適化を集中的に実施すればよい。

    コードの不吉な臭い

    リファクタリングがいつ必要になるかの正確な基準はなく、コードを書く人の嗅覚に委ねられる。以下に、リファクタリングする際の指針となる「不吉な臭い」をいくつかあげる。

    不可思議な名前

    変数、関数、クラス、モジュールなどに適切な名前をつけることは、明快なコードにするために最も重要であるが、同時に最も難しいことでもある。適切な名前がつけられれば、設計の8割が完成したと言ってもよく、適切な名前をつけようともがくことで優れた設計に近づいていく。

    コメント

    コメントを書いてはいけないと言うつもりはないが、わかりにくいコードを補うために丁寧すぎるコメントが書かれていることがある。このような場合、設計を見直して、コード自身に処理内容を表現させる(自己文章化)ようにするのが良い。

    重複したコード

    同じ意図を持つコードが2箇所以上ある場合、コードを読む労力が増えることに加えて、修正の際に重複箇所すべてに同様の修正を施す必要がある。そのため、重複箇所を関数として抽出することで、重複箇所をなくしていくべきである。

    長い関数

    関数が長くなるほど、関数内部の処理内容を理解するのは難しくなる。そこで、一つにまとまりそうな内部処理を関数として抽出し、関数を短くする。抽出した処理に適切な関数名をつけることができれば、読み手はその内部実装を読むことなく先に進むことができる。一概には言えないが、関数内部の処理が3~10行を超える場合は設計を見直したほうが良いかもしれない。

    引数が多い

    引数が多い場合、依存関係が複雑になり処理内容を理解するのは難しくなる。引数の組をオブジェクトにまとめるなどして、依存関係を整理したほうが良い。一概には言えないが、引数が3つを超える場合は設計を見直したほうが良いかもしれない。

    変更可能なグローバルデータ

    グローバル変数のような「変更可能なグローバルデータ」は、コードのどこからでも変更でき、どこで変更されたかわからないため、バグにつながりやすく、バグの発生箇所の特定を難しくする。なお、グローバル変数以外にもクラス変数やSingletonでも同様の問題が発生する。

    また、グローバルデータではなく、単なる「変更可能なデータ」でも、スコープが大きい場合には上記と同様の問題が発生する。「変更可能なデータ」は適切に関数内部に閉じ込めて、スコープを数行に限定することが望ましい。

    一対一の変更

    ある一つの修正が必要なときに、一つのモジュールを変更すれば良いという状態が望ましい。複数の修正で同一のモジュールを何度も変更しなければならない場合や、ある一つの修正で複数のモジュールを変更しなければならない場合は設計を見直す必要がある。

    誤った境界

    モジュールは内部のやり取りを最大に、外部とのやり取りを最小にするように境界づけられることが望ましい。あるモジュールの関数が、内部モジュールより外部モジュールとやりとりしている場合、関数を移動するなどしてモジュールの境界を見直す必要がある。

    データの群れ

    複数のデータが様々な箇所で一緒に現れる場合は、データの組をオブジェクトにまとめるなど依存関係を整理したほうが良い。

    基本データ型への執着

    対象のドメインを表現するのに役立つ、貨幣、座標、範囲といった型を導入せず、整数、浮動小数点数、文字列などの基本データ型に執着していることが多く見られる。ドメインを表現するのに役立つのであれば積極的に型をカスタムしたほうが良い。

    怠け者の要素

    使われていない変数、メソッドが一つしかないクラス、別のオブジェクトに処理を委譲しているだけのクラス、きっといつか必要になる機能などは削除したほうが良い。

    継承の多用

    継承では、子クラスが親クラスだけでなくすべての祖先クラスと結合していて、ある祖先クラスを変更すると容易に子クラスが壊れてしまう。とりわけ、親クラスの振る舞いは継承しているものの、インタフェースは必要としない場合は、継承を乱用している可能性が高い。このような場合、継承を用いるより他クラスに処理を委譲したほうがよい。

    参考資料


    著者画像

    ゆうき

    2018/04からITエンジニアとして活動、2021/11から独立。主な使用言語はPython, TypeScript, SAS, etc.