その関数に直接関係ない部分を別の関数にする(無関係の下位問題を抽出する - リーダブルコードより)


基本思想

  1. 関数やコードブロックを見て「このコードの高レベルの目標は何か?」と自問する。
  2. コードの各行に対して「高レベルの目標に直接的に効果があるのか? あるいは、無関係の下位問題を解決しているのか?」と自問する。
  3. 無関係の下位問題を解決しているコードが相当量あれば、それらを抽出して別の関数にする

重要なのは太字の部分で、要は「その関数に直接関係ない部分を別の関数にする」ということだと思います。

無関係の下位問題を抽出する例

findClosestLocation関数の高レベルの目標は、「与えられた地点から最も近い位置を見つける」ことです。

// 与えられた緯度経度に最も近い'array'の要素を返す。
// 地球が完全な球体であることを前提としている。
var findClosestLocation = function (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 closest;
};

このサンプルの場合、

  • 2つの地点をラジアンに変換する
  • 「球面三角法の第二余弦定理」の公式を使う

の部分が無関係の下位問題のため、抽出してspherical_distance関数とします。

var spherical_distance = function (lat1, lng1, lat2, lng2) {
    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(lat1_rad) * Math.sin(lat2_rad) +
                     Math.cos(lat1_rad) * Math.cos(lat2_rad) *
                     Math.cos(lng2_rad - lng1_rad));
};

結果的にfindClosestLocation関数は高レベルの目標である「与えられた地点から最も近い位置を見つける」ことに集中できるようになりました。

var findClosestLocation = function (lat, lng, array) {
    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;
};

コードを分割するメリット

  • 単純に可読性が向上する
  • 部品に分けることでテストがしやすくなる
  • 本質的な課題に集中できる

コードを分割することによって、単純にコードが読みやすくなるという利点があります。しかし、そのためにはある程度理解しやすい関数名をつける必要はあると思いますが。

また、部品ごとにテストがしやすくなるというメリットもあります。1つの関数が複雑で長いと、テストも複雑になってしまうでしょう。

一番重要なのは最後の本質的な課題に集中できることだと思います。先の例では、複雑な数学の計算式を別の関数とすることで、本質的な問題に集中できるようになっています。

コードを分割するデメリット

  • 分割を過度に行うと関数の処理を追うのが面倒になる

分割後の関数をさらに深い階層に分割していくのはやめておいたほうがいいと思います(縦に広げるイメージ)。

しかし、これを嫌って関数の分割をやめるのは本末転倒なので、普段関数を分けることを意識していない場合は「やりすぎかな?」などと考えすぎずに分割するのも良いかなと思います。

参考