FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

「リーダブルコード」を読む (第9章「変数と読みやすさ」第10章「無関係の下位問題を抽出する」)

諸事情で数日はネットワークをあまり使えないので、RISC-Vに関連する調査は中止。 あまりやることが無いので例によって一人読書会をしている。 今回は「リーダブルコード」。名著と呼ばれており、私も一度さっくりと読んだ。 ただし忘れていることもあるし、体系的に学びたいので読み直す。

例によってJupyter Notebookに書いてまとめをブログに張っていくことにする。

今回は第9章「変数と読みやすさ」第10章「無関係の下位問題を抽出する」までをまとめた。

まとめるといっても、本書は言いたいことを非常に簡潔に要約しており、ほぼほぼそれを写経したようなものになっているが、まあ自分で手を動かさないと分からないということで。。。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)


第9章 変数と読みやすさ

変数を適当に使うとプログラムが理解しにくくなるという問題。

  1. 変数が多いと変数を追跡するのが難しくなる。
  2. 変数のスコープが大きいとスコープを把握する時間が長くなる。
  3. 変数が頻繁に変更されると現在の値を把握するのが難しくなる。

9.1 変数を削除する

第8章で説明したのはコードが読みやすくなる変数。ここでは、コードが読みにくくなる変数の使い方を紹介する。

役に立たない一時変数

now = datetime.datetime.now()
root_message.last_view_time = now

このnowを使う意味はあるだろうか?

  • 複雑な式を分割していない。
  • より明確になっていない。datetime.datetime.now()のままでも十分に明確だ。
  • 一度しか使っていないので、重複コードの削除になっていない。

中間結果を削除する

中間結果を保存するためだけの変数は削除する。

var remove_one = function(array, value_to_remove) {
    var index_to_remove = null;
    for (var i = 0; i < array.length; i += 1) {
        if (array[i] === value_to_remove) {
            index_to_remove = i;
            break;
        }
    }
    if (index_to_remove !== null) {
        array.splice(index_to_remove, 1);
    }
};

このような中間変数は削除することができる。

var remove_one = function(array, value_to_remove) {
    for (var i = 0; i < array.length; i += 1) {
        if (array[i] === value_to_remove) {
            array.splice(i, 1);
            return;
        }
    }
};

制御フロー変数を削除する

うまくプログラミングすれば、制御フローを変更するためだけに存在する変数は削除することができる。

boolean done = false;
while (/* 条件 */ && !done) {
    ...
    if (...) {
        done = true;
        continue;
    }
}

制御フロー変数doneは削除することができる。

while (/* 条件 */) {
    ...
    if (...) {
        break;
    }
}

9.2 変数のスコープを縮める

鍵となる考え: 変数のことが見えるコード行数をできるだけ減らす

一度に考える必要のある変数の数を減らすため、変数のスコープをなるべく縮める。

例えば、C++で「メンバ変数」というのは、ミニグローバルの変数になっていると考えることができる。 このような変数はどのメソッドが変数を変更しているかを把握するのは難しい。 このような場合名は、ローカル変数に格下げを行う。

C++のif文のスコープ

以下のようなC++のコードは、infoは関数のスコープ内にあるので、またいつどのように使われるかを考えながらコードを読まなければならない。

PaymentInfo* info = database.ReadPaymentInfo();
if (info) {
    cout << "User paid: " << info->amount() << endl;
}

変数infoが必要なのは、if文の中だけであるので、C++の条件式で変数infoを宣言すればよい。

if (PaymentInfo* info = database.ReadPaymentInfo()) {
    cout << "User paid: " << info->amount() << endl;
}

JavaScriptで「プライベート」変数を作る

submitted = false; // 注意: グローバル変数

var submit_form = function (form_name) {
    if (submitted) {
        return;   // 二重投稿禁止
    }
    ...
    submitted = true;
};

submittedのようなグローバル変数を無くすためには、変数submittedクロージャで包んであげればよい。

var submit_form = (function() {
    var submitted = false;  // 注意: 以下の関数からしかアクセスできない
    
    return funciton (form_name) {
        if (submitted) {
            return;  // 二重投稿禁止
        }
        ...
        submitted = true;
    };
}());

外側の無名関数がすぐに実行されて、内側の関数を返す。

JavaScriptのグローバルスコープ

JavaScriptでは、変数の定義にvarをつけないと、その変数はグローバルスコープに入ってしまう。

<script>
   var f = function () {
       // 危険: 'i'は'var'で宣言されていない!
       for (i = 0; i < 10; i += 1) ...
   };
   f ();
</script>

JavaScriptのベストプラクティスでは、「変数を定義するときは常にvarキーワードをつけること」となっている。

PythonJavaScriptのネストしないスコープ

PythonJavaScriptでは、ブロックで定義された変数はその関数全体に「こぼれ出る」。

# ここまでexample_valueを使ってはいない。
if request:
    for value in request.values:
        if value > 0:
            example_value = value
            break
for logger in debug.loggers:
    logger.log("Example:", example_value)

この規則はバグを招きやすい。example_valueを使っている場所の「最も近い共通の祖先(ネスト的な意味で)」で変数を定義すれば、この問題を解決できるし、コードも読みやすくなる。

example_value = None

if request:
    for value in request.values:
        if value > 0:
            example_value = value;
            break

if example_value
    for logger in debug.loggers:
        logger.log("Example:", example_value)

定義の位置を下げる

もともとのC言語では、関数やブロックの戦闘で変数を定義する必要があった。 変数の定義は変数を使う直前に移動すればよい。

9.3 変数は一度だけ書き込む

変数が絶えず変更され続けると、値を追跡する難易度が格段に上がってしまう。 この問題と戦うためには、変数は一度だけ書き込むということにする。

鍵となる考え: 変数を操作する場所が増えると、現在値の判断が難しくなる

9.4 最後の例

これまでに紹介した原則を使ったものを示す。

以下のように配置された入力テキストフィールドがウェブページにあるとする。

<input type="text" id="input1" value="Dustin">
<input type="text" id="input2" value="Trevor">
<input type="text" id="input3" value="">
<input type="text" id="input4" value="Melissa">
...

setFirstEmptyInput()という関数を書き、文字列を受け取り、ウェブページにある最初の空の<input>に入力するというもの(この例では「input3」)。

まずは、本省の原則を適用していないコードがある。

var setFirstEmptyInput = function (new value) {
    var found = false;
    var i = 1;
    var elem = document.getElementById('input' + 1);
    while (elem !== null) {
        if (elem.value === '') {
            foud = true;
            break;
        }
        i++;
        elem = document.getElementById('input' + i);
    }
    if (found) element.value = new_value;
    return elem;
};
  • foundなどの中間変数は早めに返せば削除できる。
var setFirstEmptyInput = function (new value) {
    var i = 1;
    var elem = document.getElementById('input' + 1);
    while (elem !== null) {
        if (elem.value === '') {
            elem.value = new_value;
            return elem;
        }
        i++;
        elem = document.getElementById('input' + i);
    }
    return null;
};
  • elemはループ状に何度も利用されている。whileをforループに書き換えてみる。
var setFirstEmptyInput = function (new value) {
    for (var i = 0; true; i++) {
        elem = document.getElementById('input' + i);
        if (elem === null)
            return null;   // 検索失敗。空の入力フィールドは見つからなかった。
        if (elem.value === '') {
            elem.value = new_value;
            return elem;
        }
    }
};

9.5 まとめ

  • 邪魔な変数を削除する。本章では、結果をすぐに使って、「中間結果の変数」を削除する例を示した。
  • 変数のスコープをできるだけ小さくする。変数を数行のコードからしか見えない位置に移動する。
  • 一度だけ書き込む変数を使う。変数に一度だけ値を設定すれば(あるいは、constやfinalなどのイミュータブルにする方法を使えば)、コードが理解しやすくなる。

第10章 無関係の下位問題を抽出する

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

10.1 入門的な例: findClosestLocation()

このコードの高レベルの目標は「与えられた地点から最も近い場所を見つける」ということ。

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

緯度経度の球面距離を算出するコードを抽出する。

var spherical_distance = function (lat1, lng1, lat2, lng2) {
    var lat1_rad = radians(lat1);
    var lng1_rad = radions(lng1);
    var lat2_rad = radians(lat2);
    var lng2_rad = radians(lat2);
       
    // 「球面三角法の第二余弦定理」の公式を使う。
    var dist = Math.acos(Math.sin(lat1_rad) * Math.sin(lat2_rad) +
                         Math.cos(lat1_rad) * Math.cos(lat2_rad) *
                         Math.cos(lng2_rad - lng_rad));
}

// 与えられた緯度経度に最も近い'array'の要素を返す。
// 地球が完全な球体であることを前提としている。
var findClasestLocation = function (lat, lng, array) {
   var closest;
   var closest_dist = Number.MAX_VALUE;
   for (var i = 0; i < array.length; i += 1) {
       // 2つの地点をラジアンに変換する。
       if (dist < closest_dist) {
           closest = array[i];
           closest_dist = dist;
       }
   }
   return closest;
}

10.2 純粋なユーティリティコード

基本的なユーティリティは、プログラミング言語の組み込みライブラリとして実装されている。 しかし、例えばC++ではもう少し上位のライブラリが欲しい時がある。 そのようなコードは、複数のプロジェクトで使えるユーティリティコードになっていく。

10.3 その他の汎用コード

コードの高レベルの目標と、コードの大部分の処理が異なっている場合は、コードを分割する。

思いもよらない恩恵

コードが独立していれば、そのコードの改善が楽になる。

10.4 汎用コードをたくさん作る

汎用コードをたくさん作ることにより、プロジェクトから完全に切り離されたコードを作ることができる。

10.5 プロジェクトに特化した機能

抽出する下位問題というのは、プロジェクトから完全に独立したものである方がいい。 ただし完全に独立していなくても、それはそれで問題ない。 下位問題を取り除くだけでも効果がある。

ビジネスレビューサイトでは、urlをクリーンなバージョンに変更する。 このコードの大部分は、名前を有効なURLに変更することである。これを抽出することでコードがクリーンになる。

10.6 既存のインタフェースを簡潔にする

インタフェースをきれいにするとコードが優雅に見える。簡潔で、しかも強力。

インタフェースがきれいじゃなくても、自分で「ラップ関数」を作ることができる。

10.7 必要に応じてインタフェースを整える

プログラムの多くのコードは、その他のコードを支援するためだけに存在する。 このような「グルー」コードは、プログラムの本質的なロジックとは関係ないことが多い。別の関数に分離するとよい。

10.8 やりすぎ

無関係の下位問題を積極的に見つけて抽出することも、やりすぎる可能性がある。

小さな関数を作りすぎると、逆に読みにくくなってしまう。 新しい関数コードに追加すると、ごくわずかに(でも確実に)読みにくさのコストが発生する。

10.9 まとめ

プロジェクト固有のコードから汎用コードを分離することで、コードを汎用化できる。

プロジェクトのほかの部分から分離された、境界線の明確な小さな問題に集中できる。 こうした下位問題に対する解決策は、より緻密で正確なものになる。 それに、あとでコードを再利用できるかもしれない。