講義Wiki: リファクタリング最初の例 - tanakakenji/Rinko GitHub Wiki

1. はじめに:なぜリファクタリングを学ぶのか?

皆さん、こんにちは。本日の講義では「リファクタリング」について、具体的なコードの例を通して学んでいきます。

教科書的に「リファクタリングとは何か」という原則から説明することもできますが、多くの場合、退屈に感じてしまうものです。そこで、この講義ではまず動いているコードを見て、それを一歩ずつ改善していくプロセスを体験することから始めます。例を見ることで、リファクタリングがなぜ必要なのか、そしてどのように役立つのかを肌で感じることができるでしょう。

2. 題材となるプログラムの紹介

今回、私たちが扱うのは、ある劇団の請求書を作成するプログラムです。

この劇団は、顧客からの依頼に応じて「悲劇」や「喜劇」といった演劇を上演します。料金は、演劇の種類と観客の数によって決まります。また、常連客を増やすために、料金とは別に「ボリューム特典」としてポイントを付与する仕組みもあります。

2.1. データ構造

このプログラムでは、演劇の情報と請求の情報を JSON という形式のファイルで管理しています。

【初心者向け解説】JSONとは? JSON (JavaScript Object Notation) は、データを表現するための一つの書き方(フォーマット)です。"key": "value" のように、項目名と値をペアで記述するのが特徴で、人間にもコンピュータにも分かりやすいことから、広く使われています。

演劇データ (plays.json) どの演劇がどんな種類(悲劇か喜劇か)なのかを定義しています。

{
  "hamlet": {"name": "Hamlet", "type": "tragedy"},
  "as-like": {"name": "As You Like It", "type": "comedy"},
  "othello": {"name": "Othello", "type": "tragedy"}
}

請求データ (invoices.json) どの顧客(customer)が、どの演劇(playID)を、何人の観客(audience)で上演したかの記録です。

[
  {
    "customer": "BigCo",
    "performances": [
      {
        "playID": "hamlet",
        "audience": 55
      },
      {
        "playID": "as-like",
        "audience": 35
      },
      {
        "playID": "othello",
        "audience": 40
      }
    ]
  }
]

3. リファクタリング前のコード(スタート地点)

そして、こちらが請求書の内容を文字列として生成するプログラムの全体像です。このコードが私たちの「スタート地点」となります。

【初心者向け解説】コードの基本要素

  • function:特定の処理をまとめたもので、ここでは statement という名前の関数です。
  • let, const:変数を宣言するためのキーワードです。let は後から値を変更できますが、const は一度入れた値を変更できません。
  • for (let perf of invoice.performances)invoice.performances というリストの中から、上演記録を一つずつ perf という変数に取り出して繰り返し処理を行います。
  • switch (play.type)play.type の値("tragedy" や "comedy")によって、処理を分岐させています。
  • Intl.NumberFormat(...):数値をドル表記のような特定の書式(フォーマット)の文字列に変換するための、JavaScriptの便利な機能です。
function statement (invoice, plays) {
  let totalAmount = 0;
  let volumeCredits = 0;
  let result = `Statement for ${invoice.customer}\n`;
  const format = new Intl.NumberFormat("en-US",
                        { style: "currency", currency: "USD",
                          minimumFractionDigits: 2 }).format;

  for (let perf of invoice.performances) {
    const play = plays[perf.playID];
    let thisAmount = 0;

    switch (play.type) {
    case "tragedy":
      thisAmount = 40000;
      if (perf.audience > 30) {
        thisAmount += 1000 * (perf.audience - 30);
      }
      break;
    case "comedy":
      thisAmount = 30000;
      if (perf.audience > 20) {
        thisAmount += 10000 + 500 * (perf.audience - 20);
      }
      thisAmount += 300 * perf.audience;
      break;
    default:
        throw new Error(`unknown type: ${play.type}`);
    }

    // ボリューム特典のポイントを加算
    volumeCredits += Math.max(perf.audience - 30, 0);
    // 喜劇のときは10人につき、さらにポイントを加算
    if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);

    // 注文の内訳を出力
    result += `  ${play.name}: ${format(thisAmount/100)} (${perf.audience} seats)\n`;
    totalAmount += thisAmount;
  }
  result += `Amount owed is ${format(totalAmount/100)}\n`;
  result += `You earned ${volumeCredits} credits\n`;
  return result;
}

このコードを先ほどのデータで実行すると、以下のような請求書が出力されます。正常に動作はしていますね。

Statement for BigCo
  Hamlet: $650.00 (55 seats)
  As You Like It: $580.00 (35 seats)
  Othello: $500.00 (40 seats)
Amount owed is $1,730.00
You earned 47 credits

4. なぜこのコードをリファクタリングするのか?

「プログラムは動いているのだから、何が問題なの?」と思うかもしれません。

確かに、コンピュータはこのコードを問題なく実行できます。しかし、このコードを将来変更する必要が出てきたとき、困るのは人間です。

例えば、次のような変更要求が来たとしましょう。

  1. 請求書をHTML形式で出力したい。
  2. 「史劇」や「牧歌劇」など、新しい演劇の種類を追加したい。

現状のコードは、すべての計算ロジックが statement という一つの大きな関数に詰め込まれています。この状態で新しい機能を追加しようとすると、コードはさらに複雑になり、どこを変更すればよいか理解するのが難しくなります。その結果、間違えてバグを埋め込んでしまう可能性が非常に高くなります。

💡 構造的に機能を追加しにくいプログラムに新規機能を追加しなければならない場合には、まず機能追加が簡単になるようにリファクタリングをしてから追加すること。

良いプログラムとは、ただ動くだけでなく、将来の変更が容易で、人間にとって理解しやすい構造を持っているプログラムなのです。そのために、私たちはリファクタリングを行います。

5. リファクタリングの実践

5.1. 第一歩:テストの準備

リファクタリングを始めるとき、最初に行うことは常に同じです。それは**「しっかりとしたテスト群を用意すること」**です。

リファクタリングはコードの振る舞いを変えずに内部構造を改善する作業ですが、人間がやる以上、間違いは起こり得ます。変更によって意図せずプログラムを壊してしまう(バグを生む)ことを防ぐために、変更を加えるたびにテストを実行し、何も壊れていないことを確認できるようにします。これが安全にリファeクタリングを進めるための命綱になります。

5.2. statement 関数の分割:関数の抽出 (Extract Function)

では、いよいよコードに手を入れていきましょう。最初のステップとして、この巨大な statement 関数を、より小さく意味のある単位に分割していきます。

まず、switch 文で演目ごとの料金を計算している部分に注目します。この「料金計算」という一つのまとまった処理を、別の関数として切り出してみましょう。この手法を**「関数の抽出 (Extract Function)」**と呼びます。

// 【変更前】statement関数の一部
switch (play.type) {
case "tragedy":
  thisAmount = 40000;
  if (perf.audience > 30) {
    thisAmount += 1000 * (perf.audience - 30);
  }
  break;
// ... (以下略)

この部分を amountFor という新しい関数に切り出します。

// 【変更後】amountFor関数として抽出
function amountFor(perf, play) {
  let thisAmount = 0;
  switch (play.type) {
  case "tragedy":
    thisAmount = 40000;
    if (perf.audience > 30) {
      thisAmount += 1000 * (perf.audience - 30);
    }
    break;
  case "comedy":
    thisAmount = 30000;
    if (perf.audience > 20) {
      thisAmount += 10000 + 500 * (perf.audience - 20);
    }
    thisAmount += 300 * perf.audience;
    break;
  default:
    throw new Error(`unknown type: ${play.type}`);
  }
  return thisAmount; // 計算結果を返す
}

そして、元の statement 関数から、この新しい amountFor 関数を呼び出すように変更します。

// 【変更後】statement関数
function statement (invoice, plays) {
  // ... (前略)
  for (let perf of invoice.performances) {
    const play = plays[perf.playID];
    // ★★★ ここが変更点 ★★★
    let thisAmount = amountFor(perf, play);
    // ... (後略)
  }
  // ... (後略)
  return result;
}

これで、料金計算のロジックが一つの場所にまとまり、statement 関数は少しスッキリしました。この小さな変更が終わったら、すぐにテストを実行して、何も壊れていないことを確認します。

5.3. 可読性の向上:変数名を分かりやすくする

次に、抽出した amountFor 関数をさらに分かりやすくしていきましょう。

プログラミングにおいて、変数名や関数名はコードの「意図」を伝える非常に重要な要素です。

💡 コンパイラがわかるコードは誰にでも書ける。すぐれたプログラムは人間にとってわかりやすいコードを書く。

まず、amountFor 関数内の変数 thisAmountresult に変更します。関数の処理結果を返す変数の名前を result(結果)に統一することで、「この変数が関数の返り値だな」と一目でわかるようになります。

// 【変更後】変数名をresultに変更
function amountFor(perf, play) {
  let result = 0; // ← thisAmountからresultに変更
  switch (play.type) {
  case "tragedy":
    result = 40000; // ← thisAmountからresultに変更
    // ... (以下、同様にすべてresultに変更)
  }
  return result; // ← thisAmountからresultに変更
}

さらに、引数(関数に渡す値)の名前も分かりやすくしましょう。perfaPerformance に変更します。performance という型(種類)の変数であることが名前に含まれていると、コードを読む人がその変数が何者なのかを理解しやすくなります。

// 【変更後】引数名をaPerformanceに変更
function amountFor(aPerformance, play) { // ← perfからaPerformanceに変更
  let result = 0;
  switch (play.type) {
  case "tragedy":
    result = 40000;
    if (aPerformance.audience > 30) { // ← perfからaPerformanceに変更
      result += 1000 * (aPerformance.audience - 30); // ← perfからaPerformanceに変更
    }
    break;
  // ... (以下、同様にすべてaPerformanceに変更)
  }
  return result;
}

このような地道な名前の変更が、コードの可読性を大きく向上させます。変更のたびにテストを実行するのを忘れずに。

5.4. 依存関係の排除:play 変数を削除する

amountFor(aPerformance, play) 関数をもう一度見てみましょう。この関数は aPerformanceplay の二つの情報を必要としています。

しかし、よく考えると play の情報は aPerformance(上演記録)から取得できます (plays[aPerformance.playID])。つまり、play をわざわざ引数として渡す必要はないのです。このように不要な引数は、コードを不必要に複雑にする原因になるため、取り除いていきましょう。

まず、aPerformance から play を取得するための小さな関数 playFor を作ります。

// 【新規追加】playを取得するためのヘルパー関数
function playFor(aPerformance) {
  return plays[aPerformance.playID];
}

次に、この新しい関数を使って statement 関数内の play 変数を置き換えます。

// 【変更後】statement関数
function statement (invoice, plays) {
  // ... (前略)
  for (let perf of invoice.performances) {
    // ★★★ ここが変更点 ★★★
    const play = playFor(perf); // 新しい関数でplayを取得
    let thisAmount = amountFor(perf, play);
    // ... (後略)
  }
  // ... (後略)
}

最後に、play 変数そのものをなくしてしまいます(変数のインライン化)。playFor(perf) を直接 amountFor に渡すようにします。

// 【変更後】さらにスッキリさせたstatement関数
function statement (invoice, plays) {
  // ... (前略)
  for (let perf of invoice.performances) {
    // ★★★ ここが変更点 ★★★
    let thisAmount = amountFor(perf, playFor(perf)); // play変数をインライン化
    // ... (後略)
  }
  // ... (後略)
}

これで、statement 関数から play という一時変数が消え、さらに見通しが良くなりました。 (※講義の続きでは、この後 amountFor 関数からも play 引数を削除する手順に進みます)


本日のまとめ

本日は、リファクタリングの最初のステップとして、以下のことを学びました。

  1. なぜリファクタリングが必要か:動くだけでなく、将来の変更に強い、理解しやすいコードにするため。
  2. テストの重要性:安全にリファクタリングを進めるための命綱。
  3. 具体的な手法
    • 関数の抽出: 大きな関数を意味のある単位で小さな関数に分割する。
    • 変数名のリネーミング: コードの意図が伝わるように名前を改善する。
    • 不要な変数の削除: コードをシンプルにし、依存関係を減らす。

一つ一つのステップは非常に小さいですが、これを積み重ねることで、コードは着実に見通しの良い構造に変わっていきます。