「リーダブルコード」を改めて読んでみて(その5)

「リーダブルコード」に関する記事の続きです。1ヶ月を越えてしまいました…悔しい。

今回の記事では第Ⅲ部の中の11章の内容に触れていきます。
10章と同様、11章もコードを実際に見て理解することが重要な内容であり、またコードの量も少し多めだったので、分割して書くことにしました。

第Ⅲ部は「コードの再構成」というテーマで、11章では以下の点にフォーカスしていました。


一度に1つのことを

11章では、「一度に1つのことを」というタイトルで、コードが1つずつタスクを行うようにする方法について言及しています。

コードが1つずつタスクを行うようにする方法として、以下のように書かれていました。

  1. コードが行っている「タスク」をすべて列挙する
  2. タスクをできるだけ異なる関数に分割する。少なくとも異なる領域に分割する


ここからは具体例を2つ取り上げ、コードが1つずつタスクを行うようにする方法を見ていきます。

例1

最初の例は、以下の仕様を満たす投票ウィジェット用の関数です。
この関数は、ユーザが投票ボタンを押した際に呼ばれます。

  • ユーザが投票ボタンを押すと、スコアが更新される
    • 投票ボタンは賛成(Up)、反対(Down)の2種類ある
    • ユーザが賛成を押すと+1、反対を押すと-1の投票となる
    • スコアは賛成と反対の投票の合計を表す
  • ユーザの状態は'Up'、'Down'、''(=無投票)の3種類ある
  • ユーザは投票内容を変更できる
    • 例えば、賛成に投票したユーザが反対に変更すると、賛成が1減って反対が1増える(=スコアは-2される)


最初に、一度に複数のタスクを行うコードを見てみます。
get_score()set_score()は元々用意された関数で、スコアへのアクセサです。

var vote_changed = function (old_vote, new_vote) {
  var score = get_score();

  if (new_vote !== old_vote) {
    if (new_vote === 'Up') {
      score += (old_vote === 'Down' ? 2 : 1);
    } else if (new_vote === 'Down') {
      score -= (old_vote === 'Up' ? 2 : 1);
    } else if (new_vote === '') {
      score += (old_vote === 'Up' ? -1 : 1);
    }
  }

  set_score(score);
};


仕様を読む限り3つめのif文に入るケースはなさそうな気がしますが笑、それはさておき、上記のコードは以下の2つのタスクを同時に行っています。

  1. 投票を数値にパースする
  2. スコアを更新する


次に、上記のコードを一度にひとつのタスクを行うようにしたコードを見てみます。
元のvote_changedを2つの関数に分割したコードです。

// 投票を数値にパースする
var vote_value = function (vote) {
  if (vote === 'Up') {
    return +1;
  }
  if (vote === 'Down') {
    return -1;
  }
  return 0;
};

// スコアを更新する
var vote_changed = function (old_vote, new_vote) {
  var score = get_score();

  score -= vote_value(old_vote); // 更新前の値を削除する
  score += vote_value(new_vote); // 更新後の値を追加する

  set_score(score);
};


このようにタスクを分割することで、行数は長くなりましたが理解しやすいコードになりました。

例2

2つめの例は、以下の仕様を満たす関数です。
この関数は、ユーザの所在地を読みやすい文字列に整形します。

  • 所在地情報は以下の4種類ある
    • LocalityName
    • SubAdministrativeAreaName
    • AdministrativeAreaName
    • CountryName
  • 4つの情報を基に「都市」と「国」を連結した文字列を返却する
    • 「都市」は以下の優先順位で情報が存在するものを1つ使用する
      • 1. LocalityName
      • 2. SubAdministrativeAreaName
      • 3. AdministrativeAreaName
    • 上記の3つの情報がすべて使用できない場合、'Middle-of-Nowhere'を使用する
    • 「国」はCountryNameが使用できない場合、'Planet Earth'を使用する


最初に、一度に複数のタスクを行うコードを見てみます。
location_infoは所在地情報を保持するディクショナリです。

var place = location_info['LocalityName'];

// 「都市」を設定する
if (!place) {
  place = location_info['SubAdministrativeAreaName'];
}
if (!place) {
  place = location_info['AdministrativeAreaName'];
}
if (!place) {
  place = 'Middle-of-Nowhere';
}

// 「国」を設定する
if (location_info['CountryName']) {
  place += ', ' + location_info['CountryName'];
} else {
  place += ', Planet Earth';
}

return place;


上記のコードは以下の4つのタスクを同時に行っています。

  1. location_infoディクショナリから値を抽出する
  2. 「都市」の優先順位を調べる。何も見つからなかったら'Middle-of-Nowhere'にする。
  3. 「国」を取得する。見つからなかったら'Planet Earth'にする。
  4. placeを更新する。


次に、上記のコードを一度にひとつのタスクを行うようにしたコードを見てみます。

var town    = location_info['LocalityName'];
var city    = location_info['SubAdministrativeAreaName'];
var state   = location_info['AdministrativeAreaName'];
var country = location_info['CountryName'];

var first_half, second_half;

// 「都市」を設定する
first_half = town || city || state || 'Middle-of-Nowhere';

// 「国」を設定する
second_half = country || 'Planet Earth';

return first_half + ', ' + second_half;


このようにタスクを分割することで、理解しやすいコードになりました。
また、仕様変更にも強いコードになっています。

例えば、以下の仕様が新たに追加されたとします。

  • アメリカの場合、CountryNameでなくAdministrativeAreaNameを可能であれば表示する


この仕様を上記の2つの関数に反映すると、それぞれ以下のようになります。

////////////////////////////
// 一度に複数のタスクを行うコード
////////////////////////////

var place = location_info['LocalityName'];

// 「都市」を設定する
if (!place) {
  place = location_info['SubAdministrativeAreaName'];
}
/*** 追加①ここから ***/
if (!place && location_info['CountryName'] !== 'USA') {
  place = location_info['AdministrativeAreaName'];
}
/*** 追加①ここまで ***/
if (!place) {
  place = 'Middle-of-Nowhere';
}

// 「国」を設定する
/*** 追加②ここから ***/
if (location_info['AdministrativeAreaName'] && location_info['CountryName'] === 'USA') {
  place += ', ' + location_info['AdministrativeAreaName'];
}
/*** 追加②ここまで ***/
else if (location_info['CountryName']) {
  place += ', ' + location_info['CountryName'];
} else {
  place += ', Planet Earth';
}

return place;
////////////////////////////
// 一度に1つのタスクを行うコード
////////////////////////////

var town    = location_info['LocalityName'];
var city    = location_info['SubAdministrativeAreaName'];
var state   = location_info['AdministrativeAreaName'];
var country = location_info['CountryName'];

var first_half, second_half;

/*** 追加ここから ***/
if (country === 'USA') {
  first_half  = town || city || 'Middle-of-Nowhere';
  second_half = state || 'USA';
}
/*** 追加ここまで ***/
else {
  first_half  = town || city || state || 'Middle-of-Nowhere';
  second_half = country || 'Planet Earth';
}

return first_half + ', ' + second_half;


このように、仕様変更があった場合にも、一度に1つのタスクを行うコードの方が変更が容易になっています。


一連の記事はこちらです。
tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com