JavaScriptビデオストアのリファクタリング

ビデオストアの請求書を計算してフォーマットするという簡単な例が、1999年に私のリファクタリングの書籍のきっかけとなりました。最新のJavaScriptで行う場合、リファクタリングにはいくつかの方向性が考えられます。ここでは、トップレベル関数、ディスパッチャーを持つネストされた関数、クラスの使用、および中間データ構造を使用した変換へのリファクタリングの4つを検討します。

2016年5月18日



何年も前、私がリファクタリング本を執筆していたとき、ビデオ(当時は店に行ってレンタルする必要がありました)のレンタルに関する顧客の請求書を計算するコードのリファクタリングの(非常に)簡単な例から始めました。最近、このリファクタリングの例、特に最新のJavaScriptで記述した場合どのように見えるかについて検討していました。

リファクタリングは、特定の方向、つまり開発チームのコーディングスタイルに合った方向にコードを改善することです。この本では、例はJavaで記述されており、Java(特に当時)は、オブジェクト指向スタイルなどの特定のコーディングスタイルを推奨していました。しかし、JavaScriptでは、どのようなスタイルを選択するかについて、より多くのオプションがあります。特にES6(Ecmascript 2015)を使用すると、JavaのようなOOスタイルを実行できますが、すべてのJavaScript専門家がそのスタイルを支持しているわけではなく、実際、多くの人がクラスを使用することを悪いことだと考えています。

この初期のビデオストアのコード

さらに詳しく調べるには、コードをいくつか紹介する必要があります。この場合、私が世紀の変わり目に書いた元の例のJavaScriptバージョンです。

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      let movie = movies[r.movieID];
      let thisAmount = 0;
  
      // determine amount for each movie
      switch (movie.code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
  
      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movie.code === "new" && r.days > 2) frequentRenterPoints++;
  
      //print figures for this rental
      result += `\t${movie.title}\t${thisAmount}\n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;
  
    return result;
  }

ここではES6を使用しています。コードは、どちらもjsonレコードのリストにすぎない2つのデータ構造で動作します。顧客レコードは次のようになります。

{
  "name": "martin",
  "rentals": [
    {"movieID": "F001", "days": 3},
    {"movieID": "F002", "days": 1},
  ]
}

ムービー構造は次のようになります。

{
  "F001": {"title": "Ran",                     "code": "regular"},
  "F002": {"title": "Trois Couleurs: Bleu",     "code": "regular"},
  // etc
}

元の本では、ムービーはJavaオブジェクト構造のオブジェクトとしてのみ存在していました。このエッセイでは、json構造をパラメーターとして渡すことを好みます。リポジトリなどのグローバルなルックアップを使用することは、このアプリケーションには適切ではないと仮定します。

statementメソッドは、レンタルの明細を記述した簡単なテキスト出力を出力します。

Rental Record for martin
  Ran 3.5
  Trois Couleurs: Bleu 2
Amount owed is 5.5
You earned 2 frequent renter points

この出力は、サンプルコードの基準から見ても粗末です。数字をまともにフォーマットするのにすら手間をかけることができなかったのか?ただし、本はJava 1.1で書かれており、String.formatが言語に追加される前だったことを思い出してください。それは、私の怠惰を部分的に許すかもしれません。

statement関数は、長いメソッドの臭いがする例です。そのサイズだけでも私を疑心暗鬼にさせます。しかし、コードの臭いが悪いからといって、それだけでリファクタリングする理由にはなりません。ファクタリングが不十分なコードは、理解するのが難しいため問題となります。理解するのが難しいコードは、新機能を追加する場合でもデバッグする場合でも、変更が困難です。したがって、コードを読み取り理解する必要がない場合は、その構造が悪いことで害を及ぼすことはなく、しばらくの間はそのままにしておいても構いません。したがって、このコードフラグメントに関心を持つようにするには、変更する理由が必要です。私が本で使用した理由は、statementメソッドのHTMLバージョンを作成することであり、次のようなものを出力することです。

<h1>Rental Record for <em>martin</em></h1>
<table>
  <tr><td>Ran</td><td>3.5</td></tr>
  <tr><td>Trois Couleurs: Bleu</td><td>2</td></tr>
</table>
<p>Amount owed is <em>5.5</em></p>
<p>You earned <em>2</em> frequent renter points</p>

前述したように、このエッセイでは、このコードをリファクタリングして、追加の出力レンダリングを容易にするいくつかの方法を検討します。これらはすべて同じように開始します。つまり、単一のメソッドを、ロジックのさまざまな部分をキャプチャする一連の関数に分解することです。この分解が完了したら、これらの関数を配置して代替レンダリングをサポートできる4つの異なる方法を検討します。

複数の関数への分解

このような過度に長い関数を使用する場合は常に、私の最初の考えは、コードの論理的なチャンクを探し、メソッド抽出を使用して独自の関数に変えることです。[1]私の目に最初に留まるそのようなチャンクはswitchステートメントです。

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      let movie = movies[r.movieID];
      let thisAmount = 0;
  
      // determine amount for each movie
      switch (movie.code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
  
      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movie.code === "new" && r.days > 2) frequentRenterPoints++;
  
      //print figures for this rental
      result += `\t${movie.title}\t${thisAmount}\n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;
  
    return result;
  }

私のIDE(IntelliJ)は、このリファクタリングを自動的に実行することを提案していますが、正しく実行されません。JavaScriptの機能は、Javaのリファクタリングほど堅牢でも成熟もしていません。そこで、私はこれを手動で行います。これには、候補の抽出で使用されるデータを確認することが含まれます。そこには3つのデータがあります

  • thisAmountは、抽出されたコードによって計算される値です。関数内で初期化し、最後に返すことができます。
  • rは、ループで調べているレンタルです。これをパラメーターとして渡すことができます。
  • movieは、レンタルのムービーであり、以前に作成された一時的なものです。このような一時的な変数は、手続き型コードをリファクタリングするときに邪魔になることが多いため、一時変数をクエリに置き換えるを使用して、抽出されたコード内で呼び出すことができる関数に変えることをお勧めします。

一時変数をクエリに置き換えるを実行すると、コードは次のようになります。

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      let thisAmount = 0;
  
      // determine amount for each movie
      switch (movieFor(r).code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
  
      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
  
      //print figures for this rental
      result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;
  
    return result;
  
    function movieFor(rental) {return movies[rental.movieID];}
  }

次に、switchステートメントを抽出します。

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
  
    for (let r of customer.rentals) {
      const thisAmount = amountFor(r);
  
      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
  
      //print figures for this rental
      result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;
  
    return result;
  
    function movieFor(rental) {return movies[rental.movieID];}
  
    function amountFor(r) {
      let thisAmount = 0;
  
      // determine amount for each movie
      switch (movieFor(r).code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
      return thisAmount;
    }
  }

次に、頻繁なレンタルポイントの計算に注目します。同様のコード抽出を実行できます

function statement(customer, movies) {
  let totalAmount = 0;
  let frequentRenterPoints = 0;
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    const thisAmount = amountFor(r);
    frequentRenterPointsFor(r);

    //print figures for this rental
    result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
    totalAmount += thisAmount;
  }
  // add footer lines
  result += `Amount owed is ${totalAmount}\n`;
  result += `You earned ${frequentRenterPoints} frequent renter points\n`;

  return result;

  function frequentRenterPointsFor(r) {
   //add frequent renter points
    frequentRenterPoints++;
    // add bonus for a two day new release rental
    if (movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
  }

関数を抽出しましたが、親スコープの変数を更新するという動作は好みません。このような副作用はコードを推論しにくくするため、本体に副作用がないように変更します。

function statement(customer, movies) {
  let totalAmount = 0;
  let frequentRenterPoints = 0;
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    const thisAmount = amountFor(r);
    frequentRenterPoints += frequentRenterPointsFor(r);

    //print figures for this rental
    result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
    totalAmount += thisAmount;
  }
  // add footer lines
  result += `Amount owed is ${totalAmount}\n`;
  result += `You earned ${frequentRenterPoints} frequent renter points\n`;

  return result;

  function frequentRenterPointsFor(r) {
    let result = 1;
    if (movieFor(r).code === "new" && r.days > 2) result++;
    return result;
  }

抽出された2つの関数を理解している間に、少しクリーンアップする機会を利用します。

  function amountFor(rental) {
    let result = 0;
    switch (movieFor(rental).code) {
      case "regular":
        result = 2;
        if (rental.days > 2) {
          result += (rental.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = rental.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (rental.days > 3) {
          result += (rental.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }

  function frequentRenterPointsFor(rental) {
    return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
   }

これらの関数、特にamountForについては、さらにできることがあり、それは私が本の中で行ったことでした。ただし、このエッセイでは、これらの関数の本体については詳しく調べません。

それが完了したら、関数の本体に戻ります。

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      const thisAmount = amountFor(r);
      frequentRenterPoints += frequentRenterPointsFor(r);
  
      //print figures for this rental
      result += `\t${movieFor(r).title}\t${thisAmount}\n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;
  
    return result;

私がよく使う一般的な戦術は、変更可能な変数を削除することです。ここには3つあり、1つは最終的な文字列を収集するもので、他の2つはその文字列で使用される値を計算するものです。私は最初のものには問題ありませんが、他の2つは削除したいと考えています。それを開始するには、ループを分割する必要があります。まず、ループを単純化し、constをインライン化します。

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      frequentRenterPoints += frequentRenterPointsFor(r);
      result += `\t${movieFor(r).title}\t${amountFor(r)}\n` ;
      totalAmount += amountFor(r);
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;
  
    return result;

次に、ループを3つの部分に分割します。

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      frequentRenterPoints += frequentRenterPointsFor(r);
    }
    for (let r of customer.rentals) {
      result += `\t${movieFor(r).title}\t${amountFor(r)}\n`;
    }
    for (let r of customer.rentals) {
      totalAmount += amountFor(r);
    }
  
    // add footer lines
    result += `Amount owed is ${totalAmount}\n`;
    result += `You earned ${frequentRenterPoints} frequent renter points\n`;
  
    return result;

一部のプログラマーは、このようなリファクタリングのパフォーマンスへの影響を懸念しています。その場合は、ソフトウェアのパフォーマンスに関する古いが適切な記事をご覧ください。

この分割により、計算用の関数を抽出できます。

  function statement(customer, movies) {
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      result += `\t${movieFor(r).title}\t${amountFor(r)}\n`;
    }
    result += `Amount owed is ${totalAmount()}\n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
    return result;
  
    function totalAmount() {
      let result = 0;
      for (let r of customer.rentals) {
        result += amountFor(r);
      }
      return result;
    }
    function totalFrequentRenterPoints() {
      let result = 0;
      for (let r of customer.rentals) {
        result += frequentRenterPointsFor(r);
      }
      return result;
    }

コレクションパイプラインのファンなので、ループを調整してそれらを使用します。

  function totalFrequentRenterPoints() {
    return customer.rentals
      .map((r) => frequentRenterPointsFor(r))
      .reduce((a, b) => a + b, 0)
      ;
  }
  function totalAmount() {
    return customer.rentals
      .reduce((total, r) => total + amountFor(r), 0);
  }

これら2つのパイプラインスタイルのどちらを最も好むかはわかりません。

構成された関数の検証

さて、現状を見てみましょう。以下にすべてのコードを示します。

function statement(customer, movies) {
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    result += `\t${movieFor(r).title}\t${amountFor(r)}\n`;
  }
  result += `Amount owed is ${totalAmount()}\n`;
  result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
  return result;

  function totalFrequentRenterPoints() {
    return customer.rentals
      .map((r) => frequentRenterPointsFor(r))
      .reduce((a, b) => a + b, 0)
      ;
  }
  function totalAmount() {
    return customer.rentals
      .reduce((total, r) => total + amountFor(r), 0);
  }
  function movieFor(rental) {
    return movies[rental.movieID];
  }
  function amountFor(rental) {
    let result = 0;
    switch (movieFor(rental).code) {
      case "regular":
        result = 2;
        if (rental.days > 2) {
          result += (rental.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = rental.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (rental.days > 3) {
          result += (rental.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }
  function frequentRenterPointsFor(rental) {
    return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
  }
}

これで、適切に構成された関数ができました。関数のコアコードは7行で、すべて出力文字列のフォーマットに関するものです。すべての計算コードは、独自のネストされた関数のセットに移動され、それぞれの関数は小さく、その目的を示すために明確に名前が付けられています。

しかし、私はまだhtml出力関数を作成できる状態ではありません。分解された関数はすべて、全体的なstatement関数の中にネストされており、これにより、関数スコープ内の名前(amountFormovieForを呼び出すなど)と、指定されたパラメーターcustomermovieを参照できるため、関数を抽出するのが容易になります。ただし、これらの関数を参照する単純なhtmlStatement関数を作成することはできません。同じ計算を使用して異なる出力をサポートできるようにするには、さらにリファクタリングを行う必要があります。ここで、コードのファクタリング方法に応じて、どのリファクタリングを実行するかについて、いくつかのオプションがあります。次に、これらの各アプローチを順に説明し、それぞれがどのように機能するかを説明し、4つすべてが完了したら、それらを比較します。

パラメーターを使用した出力の決定

1つのルートとして、出力形式をstatement関数の引数として指定する方法があります。パラメーターの追加を使用してこのリファクタリングを開始し、既存のテキストフォーマットコードを抽出し、パラメーターがそれを指示したときに抽出された関数にディスパッチするコードを先頭に記述します。

function statement(customer, movies, format = 'text') {
  switch (format) {
    case "text":
      return textStatement();
  }
  throw new Error(`unknown statement format ${format}`);

  function textStatement() {
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      result += `\t${movieFor(r).title}\t${amountFor(r)}\n`;
    }
    result += `Amount owed is ${totalAmount()}\n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
    return result;
  }

次に、html生成関数を作成し、ディスパッチャーに句を追加できます。

  function statement(customer, movies, format = 'text') {
    switch (format) {
      case "text":
        return textStatement();
      case "html":
        return htmlStatement();
    }
    throw new Error(`unknown statement format ${format}`);
  
    function htmlStatement() {
      let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
      result += "<table>\n";
      for (let r of customer.rentals) {
        result += `  <tr><td>${movieFor(r).title}</td><td>${amountFor(r)}</td></tr>\n`;
      }
      result += "</table>\n";
      result += `<p>Amount owed is <em>${totalAmount()}</em></p>\n`;
      result += `<p>You earned <em>${totalFrequentRenterPoints()}</em> frequent renter points</p>\n`;
      return result;
    }

ディスパッチャーロジックにデータ構造を使用してみるかもしれません。

function statement(customer, movies, format = 'text') {
  const dispatchTable = {
    "text": textStatement,
    "html": htmlStatement
  };
  if (undefined === dispatchTable[format]) throw new Error(`unknown statement format ${format}`);
  return dispatchTable[format].call();

トップレベル関数の使用

トップレベルのhtmlステートメント関数を作成する際の問​​題は、計算関数がテキストステートメント関数の中にネストされていることです。したがって、進めるための明らかな方法は、それらをトップコンテキストに移動することです。

これを行うには、まず他の関数を参照しない関数を探します。この場合はmovieForです。

関数を移動する場合は常に、まず関数を新しいコンテキストにコピーし、そのコンテキストに合わせて調整してから、元の関数本体を移動した関数への呼び出しに置き換えるようにします。

function topMovieFor(rental, movies) {
  return movies[rental.movieID];
}
function statement(customer, movies) {
  // [snip]

  function movieFor(rental) {
    return topMovieFor(rental, movies);
  }

  function frequentRenterPointsFor(rental) {
    return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
  }

この時点でコンパイルしてテストできます。これにより、コンテキストの変更によって問題が発生したかどうかがわかります。それが完了したら、転送関数をインライン化できます。

function movieFor(rental, movies) {
  return movies[rental.movieID];
}
function statement(customer, movies) {
  // [snip]

  function frequentRenterPointsFor(rental) {
    return (movieFor(rental, movies).code === "new" && rental.days > 2) ? 2 : 1;
  }

amountFor内にも同様の変更があります

インライン化だけでなく、トップレベルの関数を古い名前に一致するように名前を変更したため、唯一の違いはmoviesパラメーターだけになりました。

次に、すべてのネストされた関数でそれを行います。

function statement(customer, movies) {
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    result += `\t${movieFor(r, movies).title}\t${amountFor(r, movies)}\n`;
  }
  result += `Amount owed is ${totalAmount(customer, movies)}\n`;
  result += `You earned ${totalFrequentRenterPoints(customer, movies)} frequent renter points\n`;
  return result;
}
function totalFrequentRenterPoints(customer, movies) {
  return customer.rentals
    .map((r) => frequentRenterPointsFor(r, movies))
    .reduce((a, b) => a + b, 0)
    ;
}
function totalAmount(customer, movies) {
  return customer.rentals
    .reduce((total, r) => total + amountFor(r, movies), 0);
}
function movieFor(rental, movies) {
  return movies[rental.movieID];
}
function amountFor(rental, movies) {
  let result = 0;
  switch (movieFor(rental, movies).code) {
    case "regular":
      result = 2;
      if (rental.days > 2) {
        result += (rental.days - 2) * 1.5;
      }
      return result;
    case "new":
      result = rental.days * 3;
      return result;
    case "childrens":
      result = 1.5;
      if (rental.days > 3) {
        result += (rental.days - 3) * 1.5;
      }
      return result;
  }
  return result;
}
function frequentRenterPointsFor(rental, movies) {
  return (movieFor(rental, movies).code === "new" && rental.days > 2) ? 2 : 1;
}

これで、htmlステートメント関数を簡単に作成できます

function htmlStatement(customer, movies) {
  let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
  result += "<table>\n";
  for (let r of customer.rentals) {
    result += `  <tr><td>${movieFor(r, movies).title}</td><td>${amountFor(r, movies)}</td></tr>\n`;
  }
  result += "</table>\n";
  result += `<p>Amount owed is <em>${totalAmount(customer, movies)}</em></p>\n`;
  result += `<p>You earned <em>${totalFrequentRenterPoints(customer, movies)}</em> frequent renter points</p>\n`;
  return result;
}

部分適用されたローカル関数の宣言

このようなグローバル関数を使用する場合、パラメーターリストがかなり長くなる可能性があります。したがって、グローバル関数をパラメーターの一部またはすべてを入力して呼び出すローカル関数を宣言すると便利な場合があります。グローバル関数の部分適用であるローカル関数は、後で使用できます。JavaScriptでこれを行うには、さまざまな方法があります。1つは、ローカル関数を変数に割り当てることです。

  function htmlStatement(customer, movies) {
    const amount = () => totalAmount(customer, movies);
    const frequentRenterPoints = () => totalFrequentRenterPoints(customer, movies);
    const movie = (aRental) => movieFor(aRental, movies);
    const rentalAmount = (aRental) =>  amountFor(aRental, movies);
    let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
    result += "<table>\n";
    for (let r of customer.rentals) {
      result += `  <tr><td>${movie(r).title}</td><td>${rentalAmount(r)}</td></tr>\n`;
    }
    result += "</table>\n";
    result += `<p>Amount owed is <em>${amount()}</em></p>\n`;
    result += `<p>You earned <em>${frequentRenterPoints()}</em> frequent renter points</p>\n`;
    return result;
  }

もう1つは、ネストされた関数として宣言することです。

  function htmlStatement(customer, movies) {
    let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
    result += "<table>\n";
    for (let r of customer.rentals) {
      result += `  <tr><td>${movie(r).title}</td><td>${rentalAmount(r)}</td></tr>\n`;
    }
    result += "</table>\n";
    result += `<p>Amount owed is <em>${amount()}</em></p>\n`;
    result += `<p>You earned <em>${frequentRenterPoints()}</em> frequent renter points</p>\n`;
    return result;
  
    function amount() {return totalAmount(customer, movies);}
    function frequentRenterPoints() {return totalFrequentRenterPoints(customer, movies);}
    function rentalAmount(aRental) {return amountFor(aRental, movies);}
    function movie(aRental) {return movieFor(aRental, movies);}
  }

さらに別のアプローチは、bindを使用することです。それは自分で調べてみてください。これらの形式の方が理解しやすいので、ここでは使用しません。

クラスの使用

オブジェクト指向には慣れているので、クラスやオブジェクトを検討するのは当然のことです。ES6 では、古典的な OO のための優れた構文が導入されました。この例にどのように適用するかを見ていきましょう。

最初のステップは、顧客から始めてデータをオブジェクトでラップすることです。

customer.es6…

  export default class Customer {
    constructor(data) {
      this._data = data;
    }
  
    get name() {return this._data.name;}
    get rentals() { return this._data.rentals;}
  }

statement.es6…

  import Customer from './customer.es6';
  
  function statement(customerArg, movies) {
    const customer = new Customer(customerArg);
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      result += `\t${movieFor(r).title}\t${amountFor(r)}\n`;
    }
    result += `Amount owed is ${totalAmount()}\n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
    return result;

今のところ、このクラスは元の JavaScript オブジェクトの単純なラッパーにすぎません。次に、レンタルの同様のラッパーを作成します。

rental.es6…

  export default class Rental {
    constructor(data) {
      this._data = data;
    }
    get days() {return this._data.days}
    get movieID() {return this._data.movieID}
  }

customer.es6…

  import Rental from './rental.es6'
  
  export default class Customer {
    constructor(data) {
      this._data = data;
    }
  
    get name() {return this._data.name;}
    get rentals() { return this._data.rentals.map(r => new Rental(r));}
  }

これで、単純な json オブジェクトをラップするクラスができたので、メソッドの移動のターゲットができました。関数をトップレベルに移動するのと同様に、最初に取り組む関数は、他の関数を呼び出さない `movieFor` です。ただし、この関数にはコンテキストとして映画のリストが必要であり、新しく作成されたレンタルオブジェクトで利用できるようにする必要があります。

statement.es6…

  function statement(customerArg, movies) {
    const customer = new Customer(customerArg, movies);
    let result = `Rental Record for ${customer.name}\n`;
    for (let r of customer.rentals) {
      result += `\t${movieFor(r).title}\t${amountFor(r)}\n`;
    }
    result += `Amount owed is ${totalAmount()}\n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
    return result;

class Customer…

  constructor(data, movies) {
    this._data = data;
    this._movies = movies
  }
  get rentals() { return this._data.rentals.map(r => new Rental(r, this._movies));}

class Rental…

  constructor(data, movies) {
    this._data = data;
    this._movies = movies;
  }

サポートデータが整ったら、関数を移動できます。

statement.es6…

  function movieFor(rental) {
    return rental.movie;
  }

class Rental…

  get movie() {
    return this._movies[this.movieID];
  }

以前に行った移動と同様に、最初のステップは、新しいコンテキストにコアの動作を配置し、そのコンテキストに適合させ、元の関数が新しい関数を呼び出すように調整することです。これが機能したら、元の関数呼び出しをインライン化するのは比較的簡単です。

statement.es6…

function statement(customerArg, movies) {
  const customer = new Customer(customerArg, movies);
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    result += `\t${r.movie.title}\t${amountFor(r)}\n`;
  }
  result += `Amount owed is ${totalAmount()}\n`;
  result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
  return result;

  function amountFor(rental) {
    let result = 0;
    switch (rental.movie.code) {
      case "regular":
        result = 2;
        if (rental.days > 2) {
          result += (rental.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = rental.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (rental.days > 3) {
          result += (rental.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }

  function frequentRenterPointsFor(rental) {
    return (rental.movie.code === "new" && rental.days > 2) ? 2 : 1;
  }

同じ基本シーケンスを使用して、2 つの計算をレンタルにも移動できます。

statement.es6…

function statement(customerArg, movies) {
  const customer = new Customer(customerArg, movies);
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    result += `\t${r.movie.title}\t${r.amount}\n`;
  }
  result += `Amount owed is ${totalAmount()}\n`;
  result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
  return result;

  function totalFrequentRenterPoints() {
    return customer.rentals
      .map((r) => r.frequentRenterPoints)
      .reduce((a, b) => a + b, 0)
      ;
  }
  function totalAmount() {
    return customer.rentals
      .reduce((total, r) => total + r.amount, 0);
  }

class Rental…

  get frequentRenterPoints() {
    return (this.movie.code === "new" && this.days > 2) ? 2 : 1;
  }
  get amount() {
    let result = 0;
    switch (this.movie.code) {
      case "regular":
        result = 2;
        if (this.days > 2) {
          result += (this.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = this.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (this.days > 3) {
          result += (this.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }

次に、合計計算の2つの関数を顧客に移動できます。

statement.es6…

function statement(customerArg, movies) {
  const customer = new Customer(customerArg, movies);
  let result = `Rental Record for ${customer.name}\n`;
  for (let r of customer.rentals) {
    result += `\t${r.movie.title}\t${r.amount}\n`;
  }
  result += `Amount owed is ${customer.amount}\n`;
  result += `You earned ${customer.frequentRenterPoints} frequent renter points\n`;
  return result;
}

class Customer…

  get frequentRenterPoints() {
    return this.rentals
      .map((r) => r.frequentRenterPoints)
      .reduce((a, b) => a + b, 0)
      ;
  }
  get amount() {
    return this.rentals
      .reduce((total, r) => total + r.amount, 0);
  }

計算ロジックをレンタルオブジェクトと顧客オブジェクトに移動したので、ステートメントの HTML バージョンを作成するのは簡単です。

statement.es6…

  function htmlStatement(customerArg, movies) {
    const customer = new Customer(customerArg, movies);
    let result = `<h1>Rental Record for <em>${customer.name}</em></h1>\n`;
    result += "<table>\n";
    for (let r of customer.rentals) {
      result += `  <tr><td>${r.movie.title}</td><td>${r.amount}</td></tr>\n`;
    }
    result += "</table>\n";
    result += `<p>Amount owed is <em>${customer.amount}</em></p>\n`;
    result += `<p>You earned <em>${customer.frequentRenterPoints}</em> frequent renter points</p>\n`;
    return result;
  }

構文のないクラス

ES2015 のクラス構文は物議を醸しており、一部の人々は(Java 開発者に対する皮肉を込めて)必要ないと感じています。まったく同じ一連のリファクタリング手順を実行して、このような結果を出すことができます。

function statement(customerArg, movies) {
  const customer = createCustomer(customerArg, movies);
  let result = `Rental Record for ${customer.name()}\n`;
  for (let r of customer.rentals()) {
    result += `\t${r.movie().title}\t${r.amount()}\n`;
  }
  result += `Amount owed is ${customer.amount()}\n`;
  result += `You earned ${customer.frequentRenterPoints()} frequent renter points\n`;
  return result;

  
}
function createCustomer(data, movies) {
  return {
    name: () => data.name,
    rentals: rentals,
    amount: amount,
    frequentRenterPoints: frequentRenterPoints
  };

  function rentals() {
    return data.rentals.map(r => createRental(r, movies));
  }
  function frequentRenterPoints() {
    return rentals()
      .map((r) => r.frequentRenterPoints())
      .reduce((a, b) => a + b, 0)
      ;
  }
  function amount() {
    return rentals()
      .reduce((total, r) => total + r.amount(), 0);
  }

}

function createRental(data, movies) {
  return {
    days: () => data.days,
    movieID: () => data.movieID,
    movie: movie,
    amount: amount,
    frequentRenterPoints: frequentRenterPoints
  };

  function movie() {
    return movies[data.movieID];
  }

  function amount() {
    let result = 0;
    switch (movie().code) {
      case "regular":
        result = 2;
        if (data.days > 2) {
          result += (data.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = data.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (data.days > 3) {
          result += (data.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }

  function frequentRenterPoints() {
    return (movie().code === "new" && data.days > 2) ? 2 : 1;
  }


}

このアプローチでは、関数をオブジェクトとしてパターンを使用しています。コンストラクター関数(`createCustomer` および `createRental`)は、関数参照の JavaScript オブジェクト(ハッシュ)を返します。各コンストラクター関数には、オブジェクトのデータを保持するクロージャが含まれています。返された関数のオブジェクトは同じ関数コンテキストにあるため、このデータにアクセスできます。私はこれをクラス構文を使用する場合とまったく同じパターンと見なしていますが、異なる方法で実装されています。明示的な構文の方が明示的であるため、明確に考えられるため、明示的な構文を使用することを好みます。

データ変換

これらのアプローチはすべて、ステートメントの印刷関数が、必要なデータを計算するために他の関数を呼び出すことを伴っていました。このための別のアプローチは、このデータをデータ構造自体でステートメント印刷関数に渡すことです。このアプローチでは、計算関数は、印刷関数に必要なすべてのデータを持つように顧客データ構造を変換するために使用されます。

リファクタリング用語では、これはケント・ベックが昨年の夏に説明してくれた、まだ書かれていない分割フェーズのリファクタリングの例です。このリファクタリングでは、計算を中間データ構造を使用して通信する2つのフェーズに分割します。中間データ構造を導入することから、このリファクタリングを開始します。

  function statement(customer, movies) {
    const data = createStatementData(customer, movies);
    let result = `Rental Record for ${data.name}\n`;
    for (let r of data.rentals) {
      result += `\t${movieFor(r).title}\t${amountFor(r)}\n`;
    }
    result += `Amount owed is ${totalAmount()}\n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
    return result;
  
    function createStatementData(customer, movies) {
      let result = Object.assign({}, customer);
      return result;
    }

このケースでは、元の顧客データ構造を要素を追加して拡充しているため、`Object.assign` の呼び出しから開始します。変換されたデータ構造が元の構造とどれほど異なるかによって、完全に新しいデータ構造を作成することもできます。

次に、レンタル行ごとに同じことを行います。

function statement…

  function createStatementData(customer, movies) {
    let result = Object.assign({}, customer);
    result.rentals = customer.rentals.map(r => createRentalData(r));
    return result;

    function createRentalData(rental) {
      let result = Object.assign({}, rental);
      return result;
    }
  }

`createStatementData` の内部の構築方法を `createStatementData` の呼び出し側が知る必要はないため、`createRentalData` を `createStatementData` の内部にネストしていることに注意してください。

次に、変換されたデータの投入を開始し、最初にレンタルされた映画のタイトルから始めます。

function statement(customer, movies) {
  const data = createStatementData(customer, movies);
  let result = `Rental Record for ${data.name}\n`;
  for (let r of data.rentals) {
    result += `\t${r.title}\t${amountFor(r)}\n`;
  }
  result += `Amount owed is ${totalAmount()}\n`;
  result += `You earned ${totalFrequentRenterPoints()} frequent renter points\n`;
  return result;
  //…

  function createStatementData(customer, movies) {
    // …

    function createRentalData(rental) {
      let result = Object.assign({}, rental);
      result.title = movieFor(rental).title;
      return result;
    }
  }

次に、金額の計算、続いて合計の計算を行います。

function statement(customer, movies) {
  const data = createStatementData(customer, movies);
  let result = `Rental Record for ${data.name}\n`;
  for (let r of data.rentals) {
    result += `\t${r.title}\t${r.amount}\n`;
  }
  result += `Amount owed is ${data.totalAmount}\n`;
  result += `You earned ${data.totalFrequentRenterPoints} frequent renter points\n`;
  return result;

  function createStatementData(customer, movies) {
    let result = Object.assign({}, customer);
    result.rentals = customer.rentals.map(r => createRentalData(r));
    result.totalAmount = totalAmount();
    result.totalFrequentRenterPoints = totalFrequentRenterPoints();
    return result;

    function createRentalData(rental) {
      let result = Object.assign({}, rental);
      result.title = movieFor(rental).title;
      result.amount = amountFor(rental);
      return result;
    }
  }

すべての計算関数で計算結果をデータとして配置したので、ステートメントレンダリング関数から分離するために、関数を移動できます。まず、すべての計算関数を `createStatementData` の内部に移動します。

function statement (customer, movies) {
  // body …
  function createStatementData (customer, movies) {
    // body …

    function createRentalData(rental) { … }
    function totalFrequentRenterPoints() { … }
    function totalAmount() { … }
    function movieFor(rental) { … }
    function amountFor(rental) { … }
    function frequentRenterPointsFor(rental) { … }
  }
}

次に、`createStatementData` を `statement` の外部に移動します。

function statement (customer, movies) { … }

function createStatementData (customer, movies) {
  function createRentalData(rental) { … }
  function totalFrequentRenterPoints() { … }
  function totalAmount() { … }
  function movieFor(rental) { … }
  function amountFor(rental) { … }
  function frequentRenterPointsFor(rental) { … }
}

このように関数を分離したら、同じデータ構造を使用するために、ステートメントの HTML バージョンを作成できます。

function htmlStatement(customer, movies) {
  const data = createStatementData(customer, movies);
  let result = `<h1>Rental Record for <em>${data.name}</em></h1>\n`;
  result += "<table>\n";
  for (let r of data.rentals) {
    result += `  <tr><td>${r.title}</td><td>${r.amount}</td></tr>\n`;
  }
  result += "</table>\n";
  result += `<p>Amount owed is <em>${data.totalAmount}</em></p>\n`;
  result += `<p>You earned <em>${data.totalFrequentRenterPoints}</em> frequent renter points</p>\n`;
  return result;
}

また、データの計算とステートメントのレンダリングの境界をさらに明確にするために、`createStatementData` を別のモジュールに移動することもできます。

statement.es6

  import createStatementData from './createStatementData.es6';
  function htmlStatement(customer, movies) { … }
  function statement(customer, movies) { … }

createStatementData.es6

  export default function createStatementData (customer, movies) {
    function createRentalData(rental) { … }
    function totalFrequentRenterPoints() { … }
    function totalAmount() { … }
    function movieFor(rental) { … }
    function amountFor(rental) { … }
    function frequentRenterPointsFor(rental) { … }
  }

アプローチの比較

それでは、ここで立ち止まって、自分が何を手に入れたかを見てみましょう。最初に、単一のインライン関数として記述されたコードの本体があります。このコードをリファクタリングして、計算コードを複製せずに HTML レンダリングを有効にしたかったのです。私の最初のステップは、このコードを元の関数内にあるいくつかの関数に分割することでした。そこから、4 つの異なるパスを検討しました。

top-level-functions

すべての関数をトップレベル関数として記述する

    function htmlStatement(customer, movies)

    function textStatement(customer, movies)

    function totalAmount(customer, movies)

    function totalFrequentRenterPoints(customer, movies)

    function amountFor(rental, movies)

    function frequentRenterPointsFor(rental, movies)

    function movieFor(rental, movies)

コードを表示

parameter-dispatch

トップレベル関数へのパラメーターを使用して、出力する形式を指定する

    function statement(customer, movies, format)

        function htmlStatement()

        function textStatement()

        function totalAmount()

        function totalFrequentRenterPoints()

        function amountFor(rental)

        function frequentRenterPointsFor(rental)

        function movieFor(rental)

コードを表示

classes

レンダリング関数で使用されるクラスに計算ロジックを移動する

    function textStatement(customer, movies)

    function htmlStatement(customer, movies)

    class Customer

        get amount()

        get frequentRenterPoints()

        get rentals()

    class Rental

        get amount()

        get frequentRenterPoints()

        get movie()

コードを表示

transform

計算ロジックを別のネストされた関数に分割し、レンダリング関数のための中間データ構造を生成する

    function statement(customer, movies)

    function htmlStatement(customer, movies)

    function createStatementData(customer, movies)

        function createRentalData()

        function totalAmount()

        function totalFrequentRenterPoints()

        function amountFor(rental)

        function frequentRenterPointsFor(rental)

        function movieFor(rental)

コードを表示

比較のベースラインとしてトップレベル関数を使用します。これは概念的に最も単純な代替案であるためです。[2] これは、すべてのコードから呼び出し可能な純粋な関数のセットに作業を分割するため、単純です。これは、テストケースまたは REPL のいずれかを使用して、個々の関数を簡単にテストできるため、使いやすく、テストも簡単です。

トップレベル関数の欠点は、パラメーターの受け渡しが繰り返されることです。各関数には映画のデータ構造を提供する必要があり、顧客レベルの関数にも顧客構造を提供する必要があります。ここでの繰り返し入力は気にしませんが、繰り返しの読み取りは気になります。パラメーターを読み取るたびに、それらが何であるかを把握し、パラメーターが変更されているかどうかを確認する必要があります。これらすべての関数では、顧客と映画のデータは共通のコンテキストですが、トップレベル関数では、その共通コンテキストは明示的ではありません。私はプログラムを読みながらそれを推測し、頭の中でその実行モデルを構築しますが、私はできるだけ明示的なものを好みます。

この要素は、コンテキストが大きくなるにつれて重要になります。ここには 2 つのデータ項目しかありませんが、さらに多く見つかるのは珍しいことではありません。トップレベル関数のみを使用すると、すべての呼び出しで大きなパラメーターリストになり、それぞれが読解力に負担をかけます。これにより、これらのパラメーターをすべて、多くの関数のすべてのコンテキストを含むコンテキストパラメーターにバンドルするという罠につながる可能性があります。これにより、これらの関数が何をするのかが曖昧になります。ローカルで部分的に適用された関数を定義することで、この痛みを軽減できますが、それは余分な関数宣言を混ぜるのに非常に手間がかかります。これは、クライアントコードの各ビットで複製する必要があります。

他の 3 つの代替案の利点は、それらがそれぞれ共通のコンテキストを明示的にし、プログラムの構造内にそれをキャプチャすることです。パラメーターディスパッチアプローチは、トップレベルのパラメーターリストでコンテキストをキャプチャすることによってこれを行います。これにより、ネストされたすべての関数で共通のコンテキストとして利用できるようになります。これは、元のコードで特にうまく機能し、ネストされた関数を持たない言語よりも、単一の関数からネストされた関数へのリファクタリングをより簡単に行うことができます。

しかし、パラメーターディスパッチアプローチは、HTML 形式の応答など、コンテキストから異なる全体的な動作が必要になると、ぐらつき始めます。呼び出す関数を決定するために、何らかのディスパッチャーを作成する必要があります。レンダラーに形式を指定するのはそれほど悪くありませんが、このようなディスパッチロジックは明確な臭いを発します。それをどのように記述しても、それは本質的に、名前付き関数を呼び出す言語のコア機能を複製しています。私はすぐに次のナンセンスにつながる可能性のある道を歩んでいます。

function executeFunction (name, args) {
  const dispatchTable = {
    //...

この種のアプローチにはコンテキストがあります。それは、出力形式の選択がデータとして呼び出し元に渡される場合です。その場合、そのデータ項目にディスパッチメカニズムが必要です。ただし、呼び出し元がステートメント関数を次のように呼び出している場合…

const someValue = statement(customer, movieList, 'text');

…そうであるならば、コードにディスパッチロジックを記述するべきではありません。

ここでのキーは呼び出しメソッドです。関数の選択を示すためにリテラル値を使用することは、臭いを発します。この API の代わりに、呼び出し元が関数名の一部として `textStatement` または `htmlStatement` を指定するようにします。次に、言語の関数ディスパッチメカニズムを使用して、他のものを自分で組み立てることを避けることができます。

それでは、この 2 つの代替案を踏まえて、私はどこにいるのでしょうか?一部のロジックには明示的な共通コンテキストが必要ですが、そのロジックを使用して異なる操作を呼び出す必要があります。このような必要性を感じたら、すぐにオブジェクト指向の使用を思い浮かべます。これは、本質的に共通コンテキストでの一連の独立して呼び出し可能な操作です。[3] これにより、例のクラスバージョンにつながります。これにより、顧客とレンタルのオブジェクト内で、顧客と映画の共通コンテキストをキャプチャできます。オブジェクトをインスタンス化するときにコンテキストを 1 回設定すると、それ以降のすべてのロジックでその共通コンテキストを使用できます。

オブジェクトメソッドは、トップレベルの場合の部分的に適用されたローカル関数に似ていますが、ここでは共通コンテキストがコンストラクターによって提供されます。したがって、ローカル関数のみを記述し、トップレベルの関数は記述しません。呼び出し元は、コンストラクターを使用してコンテキストを示し、ローカル関数を直接呼び出します。ローカルメソッドは、オブジェクトインスタンスの共通コンテキスト上の仮想トップレベル関数の部分的な適用と考えることができます。

クラスを使用すると、さらにレンダリングロジックを計算ロジックから分離するという概念が導入されます。元の単一関数の欠点の 1 つは、両方を混在させていることです。関数に分割すると、ある程度分離されますが、それらはすべて同じ概念空間に存在します。これは少し不公平です。計算関数を 1 つのファイルに、レンダリング関数を別のファイルに配置し、適切なインポートステートメントでリンクすることができます。しかし、共通コンテキストは、ロジックをモジュールにグループ化する方法についての自然なヒントを提供することがわかりました。

私はオブジェクトを共通の部分適用(partial application)の集合として説明してきましたが、別の見方もできます。オブジェクトは入力データ構造でインスタンス化されますが、計算関数を通じて公開される計算されたデータでこのデータを強化します。私はこれをゲッターにすることで、クライアントが生データと全く同じように扱うようにし、一様アクセス原則を適用しました。これは、コンストラクタ引数からゲッターのこの仮想データ構造への変換として考えられます。変換の例は同じアイデアですが、初期データとすべての計算データを組み合わせた新しいデータ構造を作成することで実装されています。オブジェクトが顧客クラスとレンタルクラス内に計算ロジックをカプセル化するように、変換アプローチはcreateStatementDatacreateRentalData内にそのロジックをカプセル化します。基本的なリストとハッシュのデータ構造を変換するこのアプローチは、多くの関数型思考の特徴です。これにより、create…Data関数は必要なコンテキストを共有でき、レンダリングロジックは複数の出力を簡単な方法で使用できるようになります。

クラスを変換として考える場合と、変換アプローチ自体との間の小さな違いの1つは、変換計算がいつ発生するかです。変換アプローチはすべてを一度に変換するのに対し、クラスは各呼び出しで個別の変換を行います。計算がいつ発生するかを簡単に切り替えて、他と一致させることができます。クラスの場合、コンストラクタで実行することにより、すべての計算を一度に実行できます。変換の場合、中間データ構造で関数を返すことで、オンデマンドで再計算できます。ほとんどの場合、ここでのパフォーマンスの違いはわずかであり、これらの関数のいずれかが高コストである場合、最良の方法は通常、メソッド/関数を使用し、最初の呼び出し後に結果をキャッシュすることです。

つまり、4つのアプローチがありますが、私の好みは何でしょうか?私はディスパッチャーロジックを書くのが好きではないので、パラメータディスパッチアプローチは使用しません。トップレベル関数は検討の余地がありますが、共有コンテキストのサイズが大きくなるにつれて、私の好みは急速に低下します。引数が2つだけの場合でも、他の代替手段に手を伸ばすでしょう。クラスと変換アプローチのどちらかを選ぶのは難しいです。どちらも共通のコンテキストを明示的にし、関心を適切に分離する良い方法を提供します。私はケージファイトが好きではないので、おそらく彼らにティーディーウィンクスをさせて勝者を選ぶでしょう。

さらなるリファクタリング

この調査では、計算関数とレンダリング関数を配置する4つの方法を検討しました。ソフトウェアは非常に柔軟な媒体であり、これ以外にも多くのバリエーションが可能ですが、これらは私が議論するのに最も興味深いと思う4つです。

これらの関数の配置だけでなく、さらに多くのリファクタリングがあります。書籍の例では、新しい映画タイプでモデルを拡張するために、amountfrequentRenterPointの計算を分解しました。ヘッダー、行、フッターの共通パターンを抽出するなど、レンダリングコードにも変更を加えるでしょう。しかし、この記事では4つのパスについて考えるだけで十分だと思います。

私の結論(もしあれば)は、観察的に同一の計算を適切に配置するさまざまな方法があるということです。異なる言語は特定のスタイルを推奨します。元の書籍のリファクタリングはJavaで行われ、これによりクラススタイルが強く推奨されました。JavaScriptは複数のスタイルを便利にサポートしています。これはプログラマにオプションを提供するため良いことですが、プログラマにオプションを供給するため悪いことでもあります。(JavaScriptでのプログラミングの難しさの1つは、良いスタイルに関するコンセンサスがほとんどないことです。)これらの異なるスタイルを理解することは役立ちますが、それらを結びつけているものを認識することの方が重要です。名前が適切に付けられている小さな関数は、同時に、そして時間の経過とともに、さまざまなニーズをサポートするために組み合わせたり操作したりできます。共通のコンテキストはロジックをまとめることを示唆しますが、プログラミングの芸術の多くは、関心を明確な一連のそのようなコンテキストに分離する方法を決定することです。


脚注

1: リファクタリングカタログは、オブジェクト指向の語彙が普及していたときに書かれたため、関数/サブルーチン/プロシージャなどを指すために「メソッド」を使用しました。JavaScriptでは、代わりに「関数」を使用するのが賢明ですが、書籍のリファクタリングの名前を使用しています。

2: パラメータディスパッチは、その構造が元のネストされた関数セットに近いほど、より良い最初のリファクタリングになります。しかし、代替手段を比較する場合、トップレベル関数の場合の方がより良い出発点です。

謝辞

ヴィトール・ゴメスは、ES6のデフォルトパラメータ値について思い出させてくれました。

ベス・アンドレス・ベック、ビル・ウェイク、ジャ・チャオヤン、グレッグ・ドエンチ、エンリケ・ソウザ、ジェイ・フィールズ、ケビン・ヤン、マルコス・ブリゼノ、ピート・ホジソン、ライアン・ブーシェがメーリングリストでこの投稿の草稿について議論しました。

ルーベン・バーテリンクは修正すべき多くのタイプミスを通知しました。

ウド・ボルコウスキーは例のバグを指摘しました。

重要な改訂

2016年5月18日: 初版