1. 序章

開発者として、誰かがコードをリファクタリングしたとよく耳にします。 おそらく、リファクタリングも何度も行いました。

それはとても一般的な用語なので、私たちは通常それが何を意味するのかさえ考えません。 残念ながら、その真の意味を明確に理解していない人もいます。 したがって、彼らは活動をリファクタリングとしてラベル付けしますが、それはそれとはほとんど関係がありません。

このチュートリアルでは、リファクタリングの周りの空気を取り除きます。

2. リファクタリングとは何ですか?

Martin Fowlerは、Refactoringという優れた本を持っています。 私たちは彼より賢くなりたくないので、彼が提供する定義に固執します。

リファクタリングは、コードの外部動作を変更せずに内部構造を改善する方法でソフトウェアシステムを変更するプロセスです。 これは、バグが発生する可能性を最小限に抑える、統制のとれたコードのクリーンアップ方法です。 本質的に、リファクタリングすると、コードが記述された後のコードの設計が改善されます。

この引用は、リファクタリングの本質を非常によく説明しています。 その要点についてお話しましょう。

2.1. 行動

最も重要な特徴は、リファクタリングが「コードの外部動作を変更しない」ことです。これは、機能を追加または削除したり、機能を変更したりしないことを意味します。 同じ入力に対して、ソフトウェアは同じ出力を生成します。 したがって、ユーザーの観点からは、ソフトウェアは変更されませんでした。 多分パフォーマンスを除いて、しかし今のところそれを脇に置いておきましょう。 明確にするために:ユーザーは、エンドユーザー(ソフトウェアを使用する)または別の開発者(たとえば、APIまたは低レベルのコンポーネントを作成する場合)である可能性があります。

内部の動作については何も言わなかったことに注意してください。 それは外部の観点からは問題ではないからです。 文字列からダブルスペースを削除する関数が必要だとしましょう。 正規表現を使用して検索して置換するか、単純なステートマシンとループを実装することで実装できます。 関数が二重スペースを削除すると、その目的は達成されます。 したがって、実装の詳細は重要ではありません。

2.2. 構造

コードの機能を変更しない場合のリファクタリングのポイントは何ですか? リファクタリングソフトウェアは「内部構造を改善します」。 「これは、バグが発生する可能性を最小限に抑える、統制のとれたコードのクリーンアップ方法です。」 エンドユーザーに直接的な価値を提供するものではありません。 では、なぜわざわざするのでしょうか。 繰り返しになりますが、マーティン・ファウラーはそれを最もよく言います。 優れたプログラマーは、人間が理解できるコードを作成します。」

言い換えれば、それは保守性に影響を与えます。 たとえば、バグを見つけて修正したり、新しい機能を追加したりする方が速くなります。 したがって、ソフトウェアの変更を容易にすることで、エンドユーザーに一時的に価値を提供します。また、開発者の頭痛や燃え尽き症候群が少なくなります。

2.3. タイミング

最後になりましたが、「リファクタリングすると、コードが記述された後、コードの設計が改善されます」。 この引用の本質は、最初にソフトウェアを機能させる必要があるということです。 要件を満たしていない場合、コードがどれほどうまく記述されていてもかまいません。そこに着いたら、保守性を向上させることができます。

3. リファクタリングとは何ですか?

リファクタリングを定義したので、この定義を満たさないアクティビティを簡単に特定できます。

たとえば、「コードをリファクタリングしてX機能を実装しました」という文をよく耳にします。

開発者が構造を変更し、同時に新しい動作を追加したことを意味する場合、前のステートメントは誤りです。 定義を覚えておく必要があります。リファクタリングは、外部から観察可能な動作を変更しません。 新しい機能は、定義によりそれを変更します。

ソフトウェアの再構築と新しい動作の追加は直交するアクティビティである必要があります。もちろん、動作を変更できるようにコードを再構築する必要がある場合もあります。 たとえば、クラスが十分でないことに気付いた場合、代わりに継承階層が必要です。 まず、一般的なものをクラスまたはインターフェースに抽出します。 これは動作を変更せず、構造のみを変更します。 次に、2番目のクラスを追加し、それをクラス階層の一部にします。 これは既存の構造を変更しませんが、新しい要素を追加します。

上記の文は、彼女がこれらの活動を繰り返し行った場合にのみ正しい可能性があります。 再構築中は新しい機能はありません。 新しい動作を追加するときに再構築はありません。

4. 前提条件

リファクタリングの3つの主要な特徴を特定しました。

  1. 外部の振る舞いは変わりません
  2. コードの内部構造を変更します
  3. コードが要件を満たした後に行われます

3番目のポイントをどのように確認できますか? また、どうすれば1日を強制できますか? 幸い、両方に簡単な解決策があります。それはテストです。

すべてのビジネスケースを主張するための自動テストを作成する必要があります。 それらがすべて緑色の場合、ソフトウェアが要件を満たしていることがわかります。 したがって、リファクタリングの準備ができています。 テストが欠落しているため、テストも成功する可能性があることに注意してください。 または、常に緑色であるため、障害が発生している可能性があります。 しかし、これらのいずれも当てはまらず、正しく機能しているテストがあると仮定しましょう。

1点目はどうですか? ビジネスケースをテストでカバーしたので、それは簡単です。すべてのリファクタリングステップの後にテストスイート全体を実行します。 すべてのテストが緑色の場合、動作は変更されていません。 それらのいくつかが壊れた場合、2つの可能性があります。

1つ目は、リファクタリング中に外部インターフェイスを変更し、テストで変更するのを忘れたことです。 たとえば、クラスの名前を変更したり、関数の引数を削除したりします。 この場合、テストコードを変更して、テストを再実行する必要があります。 コードを変更するときにテストを維持することを決して忘れてはなりません。

2つ目のケースは、リストラ中に誤って動作を変更したことです。 私たちのテストでは、どのシナリオが壊れているかが明確に示されているため、コードを簡単に修正できるはずです。

新しい機能の追加とリファクタリングを何度でも切り替えることができることに注意してください。 リファクタリングを開始する前に、ソフトウェアがすべての要件を満たすのを待つ必要はありません(待つべきではありません)。 重要なことは、リファクタリングしたい部分をテストでカバーする必要があるということです。

5. 例

このセクションでは、リファクタリングの例をいくつか紹介します。 膨大な量のリファクタリング手法があることに注意してください。 これらの小さな例の唯一の目的は、想像しやすくすることです。

Martin Fowlerによるリファクタリングには、リファクタリング手法の包括的なリストが含まれています。 また、 refactoring.guru には、優れたリファクタリングカタログもあります。

5.1. (一見)単純なケース

最も簡単なリファクタリングの1つは、変数の名前を変更することです。 たとえば、Webアプリケーションの起動時に、アプリケーションが起動したタイトルとログを変更する必要があります。 次のJavaScriptコードで実装しました。

title = 'Refactoring';

function logStart() {
  message = 'started';
  console.log(message);
}

logStart();
document.title = title;

期待どおりに動作します。 ただし、 title は、 logStart()内の変数のよりわかりやすい名前になると判断しました。 変更された関数は次のようになります。

function logStart() {
  title = 'started';
  console.log(title);
}

ただし、コードは以前と同じようには機能しません。タイトルは「リファクタリング」ではなく「開始」されます。 理由は簡単です。 グローバルスコープでtitleという名前の変数をすでに定義しています。 logStart()関数で宣言していません。 したがって、変数の値を上書きします。

重要なのは、最も単純な場合でも、予期しない副作用を防ぐためにテストを実行することが重要です。

5.2. より複雑なケース

非常に効率的な方法で平方根計算を実装したとしましょう。 ただし、有効な入力を取得したかどうかを確認する必要があります。 私たちはこの解決策を思いつきました:

function sqrt(value) {
  if (typeof value !== 'number' || value < 0) {
    return NaN;
  }

  // the magic happens here
}

しかし、私たちはこれに満足していません。 検証条件のtypeofの部分は、より読みやすくなる可能性があります。 チェックを含む関数を作成することにし、次の条件から呼び出します。

function sqrt(value) {
  if (isNotNumber(value) || value < 0) {
    return NaN;
  }

  // the magic happens here
}

function isNotNumber(value) {
  return typeof value !== 'number';
}

このリファクタリングはextractメソッドと呼ばれます。

!isNumber(value)の代わりに、既存の Number.isNaN()を使用できた可能性があることに注意してください。 わかりやすい例を示したいと思いました。

5.3. やや高度なケース

次のJavaクラスがあるとしましょう。

class Animal {
  static final int TYPE_DOG = 1;
  static final int TYPE_CAT = 2;
  
  int type;
  
  void makeSound() {
    switch (type) {
      case TYPE_DOG:
        System.out.println("woof");
        break;
      case TYPE_CAT:
        System.out.println("meow");
        break;
    }
  }
}

ただし、この実装は好きではありません。 クラスに新しい責任を導入するときは、他のメソッドでもswitchステートメントを複製する必要があります。 また、他の動物をモデル化する場合は、すべてのswitchステートメントに新しいケースを追加する必要があります。 これにより、コードが壊れやすくなります。

代わりに、タイプコードを削除し、動物ごとに個別のサブクラスを作成することにしました。

interface Animal {
  void makeSound();
}

class Dog implements Animal {
  @Override
  void makeSound() {
    System.out.println("woof");
  }
}

class Cat implements Animal {
  @Override
  void makeSound() {
    System.out.println("meow");
  }
}

新しい責任を追加したい場合は、Animalインターフェースにメソッドを追加します。 すべてのサブクラスに実装しない限り、コードはコンパイルされません。 新しい動物を追加する場合は、Animalインターフェイスを実装するクラスを作成します。 繰り返しますが、すべてのメソッドを実装するまで、コンパイルエラーが発生します。

なぜコンパイルエラーが好きなのですか? そうすれば、事件の処理を忘れたという事実を見逃すことはできません。 もちろん、考えられるすべてのケースをテストでカバーするかどうかは問題ではありません。 しかし、多くの場合、私たちのテストはすべてを網羅しているわけではないことを私たちは知っています。

このリファクタリングはタイプコードをサブクラスに置き換えます。

6. 何をリファクタリングする必要がありますか?

6.1. 問題の特定

この時点まで、リファクタリング(それは何ですか)を定義し、その前提条件(いつ実行するか)を確認し、3つの簡単な例(実行方法)を確認しました。 しかし、何をリファクタリングする必要があるかをどうやって知るのでしょうか?

読みやすさを向上させることができるコードを見つけるたびに、リファクタリングする時が来ました。しかし、それはまだ正確な定義ではありません。 提供することはできません。 ただし、リファクタリングする必要のあるコード内の一般的な兆候を特定できます。 時々、これらの兆候は、私たちが何かをすべきだとほとんど叫びます。 たとえば、次のコードについて考えてみます。

function calculatePrice(user, product, amount) {
  // loyalty discount
  const a = user.orders.length > 10 ? 0.9 : 1;
  // amount discount
  const b = amount > 100 ? 0.9 : 1;
  // discounted price
  const c = product.price * a * b;

  return c * amount;
}

変数の上にコメントを書く代わりに、意味のある名前を付ける必要があります。

function calculatePrice(user, product, amount) {
  const loyaltyDiscount = user.orders.length > 10 ? 0.9 : 1;
  const amountDiscount = amount > 100 ? 0.9 : 1;
  const discountedPrice = product.price * loyaltyDiscount * amountDiscount;

  return discountedPrice * amount;
}

前のセクションのAnimalクラスのように、わかりにくい場合があります。

それでも、問題のあるコードを見ると、何かが正しくないと感じることがあります。 臭い。 確かに、私たちはそれらの兆候を呼びますコードの臭い 。 コードの臭いに加えて、ボブおじさんはそれらをヒューリスティックとも呼んでいます。 彼は彼の優れた本CleanCodeでコードの臭いとヒューリスティックのリストを提供しています。

6.2. パフォーマンス

以前、アプリケーションのパフォーマンスについて簡単に説明しました。 マイクロ最適化されたコードは、通常、読むのがはるかに困難です。 ただし、これが重要な最適化である場合は、パフォーマンスを低下させない場合にのみ、リファクタリングして読みやすくする必要があります。

一方、読み取り可能なコードを最適化する方がはるかに簡単です。コードの機能を理解できなければ、最適化もできないため、驚くことではありません。

多くのリファクタリングは、新しいレベルの抽象化を導入します。 たとえば、新しいメソッド呼び出し、新しいクラス、またはクラスの階層全体。 これらの抽象化には計算コストが伴います。 したがって、アプリケーションのパフォーマンスはわずかに低下します。 ただし、この減少は非常に重要ではないため、気付くことさえありません。

影響が目立つ場合でも、通常は低速ですが読みやすいコードを使用します。 その理由は、通常、コードの保守が難しいため、開発者の追加コストよりもパフォーマンスの高いハードウェアを購入する方がはるかに安価であるためです。

もちろん、例外的なケースもあります。 たとえば、組み込みシステムには限られたリソースしか付属していないことが多く、これを改善することはできません。 または、1秒あたり数百万のリクエストを処理するグローバルアプリケーションを構築する場合、これらの追加のパフォーマンス要件は大きな影響を及ぼします。 しかし、これらは比較的まれなケースです。 ほとんどの場合、読みやすさが決定要因です。

7. TDD

リファクタリングできるようにするにはテストが必要であることはすでに述べました。 TDDでは、コードの前にテストを記述してそれを実行します。 それは問題を提起します:TDDとリファクタリングは互いにどのように関連していますか?

答えは、TDDとリファクタリングは非常に密接な関係にあるということです。TDDには3つのステップがあります。

  1. 失敗したテストを書きます
  2. プロダクションコードを書くことでテストに合格します
  3. 最後に、コードやテストをリファクタリングして読みやすくします

これを赤-緑-リファクタリングサイクルと呼びます。 赤、テストが失敗しているため。 したがって、それらは赤です。 合格させると緑色になります。

これが、TDDが素晴らしいもう1つの理由です。 それはあなたが高いテストカバレッジを持つことを保証するだけではありません。 また、コードの機能を理解しながら、コードを読みやすくする必要があることも考慮しています。 結局のところ、先週書いたコードほど未知のものはありません。

8. 結論

リファクタリングは、ソフトウェアの進化の自然で不可欠な部分です。

このチュートリアルでは、それが何であるか、いつ、どのようにそれを行うことができるかを理解しました。 コードの臭いとパフォーマンスへの影響について話しました。 また、リファクタリングがTDDの基本的な部分であることもわかりました。

これで、コードをより読みやすくする時が来ました!