10章 無関係の下位問題を抽出する
10章でのアドバイスは、「無関係の下位問題を積極的に見つけて抽出すること」だ。
そのためには、以下の3点を実践すれば良い。
- 関数やコードブロックを見て「このコードの高レベルの目標は何か?」を自問する。
- コードの各行に対して「高レベルの目標に直接効果はあるか」それとも
「無関係の下位問題を解決しているか」を判断する。 - 無関係の下位問題を解決しているコードが相当量あれば、それらを抽出して別の関数にする。
以下、具体的な例で解説する。
入門的な例
以下のようなJavaScriptのコードがあるとする。
このコードの高レベルの目標は「与えられた地点から最も近い地点を見つける」ことである。
// 与えらた軽度・緯度に最も近い'array'の要素を返す。
// 地球が完全な球体であることを前提としている。
var findCloseLocation = (lat, lng, array) => {
var closest;
var closest_dist = Number.MAX_VALUE;
for (var i = 0; i < array.length; i += 1) {
// 2つの地点をラジアンに変換する。
var lat_rad = radians(lat);
var lng_rad = radians(lng);
var lat2_rad = radians(array[i].latitude);
var lng2_rad = radians(array[i].longitude);
// 「球面三角法の第二余弦定理」の公式を使う。
var dist = Math.acos(
Math.sin(lat_rad) * Math.sin(lat2_rad) +
Math.cos(lat_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng_rad));
if (dist < closest_dist) {
closest = array[i];
closest_dist = dist;
}
}
return closet;
};
ループ内のコードは、「2つの地点の球面距離を算出する」という下位問題を扱っている。
これを別の関数spherical_distance()
として抽出すると良い。
そうすると、全体を以下のように書き直すことができる。
// 2つの地点(緯度・経度)の球面距離を算出する。
const spherical_distance = (lat1, lng1, lat2, lng2) => {
//2つの地点をラジアンに変換する。
var lat1_rad = radians(lat1);
var lng1_rad = radians(lng1);
var lat2_rad = radians(lat2);
var lng2_rad = radians(lng2);
// 「球面三角法の第二余弦定理」の公式を使う。
return Math.acos(Math.sin(lat11_rad) * Math.sin(lat2_rad) +
Math.cos(lat1_rad) * Math.cos(lat2_rad) *
Math.cos(lng2_rad - lng1_rad));
};
const findClosestLocation = (lat, lng, arry) => {
var closest;
var closest_dist = Number.MAX_VALUE;
for (var i = 0; i < array.length; i += 1) {
var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude);
if (dist < closest_dist) {
closest = array[i];
closest_dist = dist;
}
}
return closest;
};
コードが読みやすくなり、高レベルの目標に集中できるようになった。
さらに、spherical_distance()
は個別にテストができる関数であり、将来的に再利用も可能だ。
純粋なユーティリティコード
例えば、以下のような基本的なタスクの実装については、プログラミング言語側で用意されている「組み込み関数」が利用できる場合が多い。
- 文字列の操作
- ハッシュテーブルの使用
- ファイルの読み書き
具体的には、ファイルの中身を全て読み込みたければ、PHPならfile_get_contents("filename")
が使える、といったように。
しかし、こうした基本的なタスクであっても、それ用の組み込み関数が用意されていない場合もある。
そういったケースでは、自分で関数を書くことが望ましい。
そうして作ったコードは、複数のプロジェクトで使えるユーティリティ(便利な)コードになっていくだろう。
その他の汎用コード
以下のJavaScriptのコードでは、Ajaxでデータをサーバに送信し、デバッグのために返ってきた値を表示している。
ajax_post({
url: 'http://XXX.com/submit',
data: data,
on_success: function (response_data) {
vat str = "{\n";
for (var key in response_data) {
str += " " + key + "=" + response_data[key] + "\n";
}
alert(str + "}");
//引き続き'response_data'の処理
}
});
このコードの高レベルの目標は「サーバーをAjaxで呼び出してレスポンスを処理する」である。
しかし、コードの大部分は「レスポンスを見やすく整形して出力する」という「無関係の下位問題」を扱っている。
この整形処理は、format_pretty(obj)という別の関数として抽出できる。
var format_pretty = function (obj) {
var str = "{\n";
for (var key in obj) {
str += " " + key + "=" + obj[key] + "\n";
}
return str + "}";
};
それにより、以下のようなメリットがある。
-
format_pretty()
を抽出したことで、元のコードが簡潔になった。 -
format_pretty()
は独立した関数のため、改善が楽で、かつ再利用も可能。
汎用コードをたくさん作る
汎用コードの素晴らしい点は、プロジェクトから完全に切り離されている点だ。
このようなコードは、開発もテストも理解も楽である。
かつ、複数のプロジェクトで再利用できるので、簡単に共有できるよう特別なディレクトリ(例:util/)を用意するのが良い。
プロジェクトに特化した機能
抽出する下位問題というのは、プロジェクトから完全に独立したものである方が良い。
ただし、完全に独立していないくても、それはそれで問題ない。
下位問題を取り除くだけでも効果がある。
10章のまとめ
- プロジェクト固有のコードから汎用コードを分離する
- ただし、やりすぎは良くない。小さい関数を作りすぎると逆に読みづらくなるため。
やみくもに分離させるのではなく、再利用できる場合に分離させることが望ましい。