「リーダブルコード」に関する記事の続きです。1ヶ月を越えてしまいました…悔しい。
今回の記事では第Ⅲ部の中の11章の内容に触れていきます。
10章と同様、11章もコードを実際に見て理解することが重要な内容であり、またコードの量も少し多めだったので、分割して書くことにしました。
第Ⅲ部は「コードの再構成」というテーマで、11章では以下の点にフォーカスしていました。
一度に1つのことを
11章では、「一度に1つのことを」というタイトルで、コードが1つずつタスクを行うようにする方法について言及しています。
コードが1つずつタスクを行うようにする方法として、以下のように書かれていました。
- コードが行っている「タスク」をすべて列挙する。
- タスクをできるだけ異なる関数に分割する。少なくとも異なる領域に分割する。
ここからは具体例を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つのタスクを同時に行っています。
- 投票を数値にパースする
- スコアを更新する
次に、上記のコードを一度にひとつのタスクを行うようにしたコードを見てみます。
元の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つのタスクを同時に行っています。
location_info
ディクショナリから値を抽出する- 「都市」の優先順位を調べる。何も見つからなかったら'Middle-of-Nowhere'にする。
- 「国」を取得する。見つからなかったら'Planet Earth'にする。
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