0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「リーダブルコード 第二部 ループとロジックの単純化」

Posted at

第7章 制御フローを読みやすくする

条件式の引数の並び順

著者見解

次の2つの条件式を比べたとき、どちらが読みやすいでしょうか。

if (length >= 10)
if (10 <= length)

多くのプログラマーは、最初の書き方の方が読みやすいと感じるはずです。一般に、左辺を「調査対象(主題)」、右辺を「比較対象」として書くと理解しやすくなります。日本語の例で言えば、「もし君が18歳以上ならば」は自然ですが、「もし18歳が君の年齢以下ならば」は不自然に感じられます。条件式でも同様に、調べたいもの(ここでは length)を左に置くことで読み手にとって直感的になります。

筆者所感

このルールは、多くのプログラマーが意識せずとも無意識に実践していることではないかと思います。なお、条件式の誤り(代入と比較の取り違え)を防ぐために、あえて定数を左側に置く書き方(いわゆる「ヨーダ条件」)を採る場合もあります。例えば誤って代入を書いてしまう次のような状況を避けるためです。

if (obj = NULL)      // 代入になってしまう誤り
if (NULL == obj)     // ヨーダ条件であれば代入ミスに気づきやすい

しかし近年では、コンパイラやIDEがこうした代入ミスを警告してくれることが多く、新しくプログラミングを学ぶ人はあまり気にしなくてもよい場合が増えています。したがって、可読性を優先するなら「調査対象を左、比較対象を右」に置く通常の書き方を基本にし、プロジェクトやチームのコーディング規約に従うのが望ましいでしょう。必要に応じてツールの警告設定を有効にしておくと安心です。

if/elseブロックの並び順

著者見解

次の2つのif/elseは意味は同じですが、並び順には読みやすさの差があります。

if (a === b) {
    // 第一のケース
} else {
    // 第二のケース
}

if (a !== b) {
    // 第二のケース
} else {
    // 第一のケース
}

読みやすさを左右する典型的な指針は次のとおりです。

  • 条件は否定形より肯定形を使う(肯定形の方が直感的に読みやすい)
  • 単純な条件を先に書く(複雑な条件は後回しにする)
  • 関心を引く・重要な条件を先に書く(読み手がまず注目すべき条件を優先する)

これらの基準は互いに衝突することがあり、その場合は状況に応じて判断する必要があります。たとえば、WebサーバでURLにクエリパラメータ expand_all が含まれるかどうかでレスポンスを切り替える場面では、読み手の関心が expand_all に向くため、それを先に書くのが自然です。

if (url.HasQueryParameter("expand_all")) {
    for (int i = 0; i < items.size(); i++) {
        items[i].Expand();
    }
    ...
} else {
    response.Render(items);
    ...
}

一方で、否定形でも「単純で注意を引く」条件であれば先に書いて良い例もあります。代表的なのはエラー処理や境界条件を早期に扱うガード節(guard clause)です。

if not file:
    # エラーをログに記録して早期リターン
    return
else:
    # 正常処理
    ...
筆者所感

この項目はかなり主観が入りやすいと感じています。肯定形を使う・単純な条件を先に書くという点は比較的客観的な指針ですが、「何に関心を引かれるか」は人によって異なります。そのため、個人の嗜好だけでスタイルを決めるとチーム内でコードの書き方がばらつきやすくなります。チーム内でコーディング規約やレビュー基準を決め、何を「関心が高い」と見るかを擦り合わせる必要があるかと思います。

三項演算子

著者見解

次の例を見ます。

timestr += (hour >= 12) ? "pm" : "am";
return exponent >= 0 ? mantissa * (1 << exponent) : mantissa / (1 << -exponent);

三項演算子(?:)は、式を簡潔に表現でき、かつ他人が理解するのに時間がかからない場合にのみ使うべきです。上の例では、1つ目の書き方は簡潔で直感的なので問題ありませんが、2つ目は読みづらくなっています。こうした場合は無理に1行にまとめようとせず、if / else を使った方が自然で可読性が高くなります。

if (exponent >= 0) {
    return mantissa * (1 << exponent);
} else {
    return mantissa / (1 << -exponent);
}
筆者所感

私も、表現が長くなりそうなときは無理に三項演算子を使う必要はない、という考えに賛成です。一方で、実務では条件が非常に単純で明快な分岐が多く、そのような場合には三項演算子を使う機会も多くあります。特に、条件ごとに同じ変数へ何度も再代入するのではなく、三項演算子で一度だけ値を決めて代入するようにすると、不変性(値が不用意に書き換えられないこと)を保てて意図が明確になります。

do/whileループを避ける

著者見解

do/while ループはブロック内のコードがまず実行され、その後にループ継続条件が評価されるため、不自然に感じられることが多いです。通常、コードは上から下へ読まれるため、条件を読んだあとにもう一度ブロックの中身を確認しなければならず、読み手の負担が増えます。多くの場合、do/while は等価な while に書き換えられます。可能であれば do/while の使用を避け、可読性の高い構造を選ぶのが賢明でしょう。

// do/while
do {
    if (node.name().equals(name)) return true;
    node = node.text();
} while (node != null && --max_length > 0);
// while
while (node != null && max_length-- > 0) {
    if (node.name().equals(name)) return true;
    node = node.text();
}

また、do/while は continue や早期の制御文と組み合わさると誤解を招きやすいという欠点もあります。例えば次のコードは一見するとループ条件に意味がありそうに見えますが、実際にはループ本体の continue によってすぐに次の反復へ移り、結果としてループは1回しか実行されません。多くのプログラマーがここで立ち止まって考えることになるでしょう。

do {
    continue;
} while (false);

もちろん、ループ本体を必ず一度は実行しなければならない明確な理由がある場合は do/while を使っても問題ありません。しかし、その必要性が曖昧であれば、while や他の構造で書き換えて可読性を優先することをお勧めします。

筆者所感

私も著者の意見に賛成です。実際、do/while を見かける機会はそれほど多くなく、条件が後ろにあることで読み手がコードを二度追う必要が生じます。コードは一目で意図が分かることが重要だと考えていますから、もし do/while を while やガード節(早期リターン)などで簡潔かつ明瞭に書き換えられるのであれば、そちらを選ぶのが適切だと思います。必要な場合に限って慎重に do/while を使うようにしてください。

関数から早く返す

著者見解
public boolean contains(String str, String subStr) {
    if (str == null || subStr == null) return false;
    if (subStr.equals("")) return true;
    ...
}

関数の冒頭で条件をチェックして早めに返す(ガード節)は良い習慣です。ガード節を使わずに同じ処理を行おうとすると、コードが深くネストして不自然になりがちです。

筆者所感

関数を早く返すことは、if 文のネストを減らし、処理の意図を明確にする点で有益だと考えています。例えば、上記の例を「1 回の return にまとめよう」として書き直すと、次のようにネストが深くなり可読性が下がります。

public boolean contains(String str, String subStr) {
    boolean result = false;
    if (str != null && subStr != null) {
        if (subStr.equals("")) {
            result = true;
        } else {
            ...
        }
    }
    return result;
}

このように冗長なネストを避けるためにも、判断できる時点で早めに return する(ガード節を用いる)ことをおすすめします。ただし、いくつかの注意点もあります。

  • リソースの解放や後処理が必要な場合(ファイルを閉じる、ロックを解放するなど)は、try/finally を使い、早期リターンが後処理を妨げないようにすること。
  • あまりにも多くの早期 return が散在すると、関数のフローを追いにくくなることがあるため、読みやすさを損なわない範囲で使う。
  • チームのコーディング規約が「単一の出口」を好む場合は、その規約に従う。

総じて、簡潔に書ける部分は早めに返してネストを減らし、必要な後処理は確実に行うように気をつける、というバランスで運用するのが良いでしょう。

悪名高きgoto

著者見解

基本的に goto は使わない方がよいとされています。構造化プログラミングの観点からは、goto による任意のジャンプはコードの追跡性と保守性を損なうため避けるべきです。C のように例外処理や RAII(リソース自動解放)の仕組みが乏しい言語では、関数の末尾にまとめたクリーンアップ処理へジャンプするパターンだけは許容されることがあります。典型的な例を示します。

if (p == NULL) goto exit;
    ...
exit:
    fclose(file1);
    fclose(file2);
    ...
    return;

このような「エラーパスから一箇所へ飛んで後処理を行う」 idiom は、リソース解放コードの重複を避ける実用的な手段として使われてきました。ただし、goto を乱用してプログラムの通常の制御フロー(ループからの脱出や分岐の代替など)に使うのは避けるべきです。

筆者所感

私は goto を使用した経験がありません。したがって、この項目での所感は控えさせていただきます。

ネストを浅くする

著者見解

深くネストしたコードは読みづらく、理解や保守が難しくなります。ネストを減らす有効な手法の一つが、失敗ケースや「ここで処理を終えられる条件」をできるだけ早く検出して関数から返す(ガード節)ことです。例えば次のように書き換えると、ネストが浅くなり処理の流れが明快になります。

// ネストの深いコード
if (user_result == SUCCESS) {
    if (permission_result != SUCCESS) {
        reply.WriteErrors(permission_result);
        reply.Done();
        return;
    }
    reply.Done();
    reply.WriteErrors("");
} else {
    reply.WriteErrors(user_result);
}
reply.Done();

// ガード節で早めに return する
if (user_result != SUCCESS) {
    reply.WriteErrors(user_result);
    reply.Done();
    return;
}
if (permission_result != SUCCESS) {
    reply.WriteErrors(permission_result);
    reply.Done();
    return;
}
reply.WriteErrors("");
reply.Done();

ループ内のネストを浅くするには、不要な入れ子を避けるために continue を使って早期に次のイテレーションへ移す(ループのガード節にする)と効果的です。以下は修正例です。

// ネストの深いコード
for (int i = 0; i < results.size(); i++) {
    if (results[i] != NULL) {
        non_null_count++;
        if (results[i]->name != "") {
            cout << "Considering candidate..." << endl;
            ...
        }
    }
}

// continue を使ってネストを浅くする
for (int i = 0; i < results.size(); i++) {
    if (results[i] == NULL) continue;        // NULL の場合は次へ
    non_null_count++;
    if (results[i]->name == "") continue;    // name が空なら次へ
    cout << "Considering candidate..." << endl;
    ...
}
筆者所感

著者が指摘するように、ネストは新しい機能や分岐を追加する際に徐々に深くなりがちで、当時の事情や意図が残らないまま放置されることが多いと私も感じています。書いた直後は「ここでのネストが最善」と思っていても、時間が経つと理由がわかりにくくなり、結果として読みにくいコードが残ってしまいがちです。追加や修正を行ったら、一歩下がって全体を見直し、ネストが深くなっていないか確かめる習慣が必要だと思いました。

実行の流れを追えるかい?

著者見解

コードは単にループや条件といった低レベルの制御構造が分かりやすいだけでは不十分で、プログラム全体の「実行パス(高レベルな流れ)」が追いやすいように設計することが重要です。スレッド、割り込みハンドラ、コールバック、例外処理などは、便利に使えば冗長性を減らし可読性を上げることもできますが、乱用すると実行の流れを把握しにくくなります。これらの仕組みは有効な手段として適宜使う一方で、システム全体のフローが複雑になりすぎないよう配慮することが大切です。

筆者所感

私もかつては細かく独自例外を定義して、発生原因を厳密に分けることを好んでいました。しかし、時間をおいてコードや実行ログを見返すと、例外が過度に粒度細かく分かれていると却って理解しにくくなる、という著者の指摘には非常に共感しました。

実務では、ユースケース(高レベルの処理)ごとにエラーを集約して扱うのが有効な場合があります。例えば、ユースケースA の内部で発生しうる例外が2種類、ユースケースB で発生しうる例外が3種類あるとします。各処理内部では詳細な例外を投げて原因を特定しやすくしておき、ユースケース境界でそれらをまとめて「ユースケース失敗(UseCaseFailedException やエラーコード)」として呼び出し元に返す、という設計は実用的です。こうすることで呼び出し側は高レベルな失敗理由だけを直感的に扱え、必要に応じて内部の詳細(原因チェーンやログ)を参照できます。

第8章 巨大な式を分割する

説明変数

著者見解

複雑な式を読みやすく分割するために、その式の意味を表す変数を挟む手法を「説明変数(explanatory variable)」と呼びます。短い例を示します。

# 分割前(1行で書かれた式)
if line.split(':')[0].strip() == "root":
    ...

# 分割後(説明変数を使う)
username = line.split(':')[0].strip()
if username == "root":
    ...

説明変数を導入することで、式の各部分が何を表しているかが明確になり、読み手は一度変数名を見ればその式の意図を把握できます。

筆者所感

説明変数は単に式を分割するだけでなく、可読性や保守性を高める効果が大きいと考えています。特に式が長く複雑になった場合、説明変数を付けることで「その値が何を表すのか」が明確になり、コードを追う時間を短縮できます。

要約変数

著者見解

式そのものを分解して説明する必要がない場合でも、式を変数に代入して「意味を要約する」ことが便利な場合があります。こうした変数を要約変数(summary variable)と呼びます。大きな式を短い名前に置き換えることで、コード全体の把握や管理が容易になります。例を示します。

// 要約変数利用前
if (request.user.id == document.owner_id) {
    ...
}

// 要約変数利用後
final boolean user_owns_document = (request.user.id == document.owner_id);
if (user_owns_document) {
    ...
}

この場合、右辺の式自体はそれほど長くはないものの、request、user、id、document、owner_id といった要素が複数あり、読む側にとっては一瞬考える必要があります。要約変数により「ユーザが文書を所持しているか」という高レベルの意図が一目で伝わります。

筆者所感

要約変数は説明変数と同様に、式を分割する効果に加えて「意図を名前で表現する」ことで可読性を大きく向上させます。特に、要約変数は「何をしているか(高レベルな意味)」を表すべきで、低レベルの実装の詳細を隠す役割があると有効です。

ド・モルガンの法則を使う

著者見解

ド・モルガンの法則は論理式を等価な形に変換して読みやすくするために有用です。典型的な形は次のとおりです。

not (a or b or c) <=> (not a) and (not b) and (not c)
not (a and b and c) <=> (not a) or (not b) or (not c)

否定(not)を内側に分配し、and と or を入れ替えることで、複雑な否定付きの条件をより直感的に書けることが多くあります。たとえば次の例のように、否定が外側にある式を展開すると読みやすくなります。

// 利用前
if (!(file_exists && !is_protected)) Error("Sorry, could not read file.");

// ド・モルガンの法則を適用した後
if (!file_exists || is_protected) Error("Sorry, could not read file.");
筆者所感

近年の IDE や静的解析ツールはこうした等価変換を検出して簡潔な表現を提案してくれることが多いので、ツールの警告やリファクタリング提案に注意を向けることが有効です。ただし、自動変換が常に最良とは限らず、元の表現の方が文脈上読みやすい場合もあります。

短絡評価の悪用

著者見解

多くの言語でブール演算子は短絡評価(ショートサーキット)を行います。例えば if (a || b) の場合、a が真であれば b は評価されません。この性質は便利ですが、評価順や副作用に依存すると複雑で分かりにくいロジックになりやすいです。著者の例を示します。

// 短絡評価を悪用したコード(読み取りにくい)
assert((!(bucket = FindBucket(key))) || !bucket->IsOccupied());

// 短絡評価を利用しない場合(読みやすい)
bucket = FindBucket(key);
if (bucket != NULL) assert(!bucket->IsOccupied());

前者は1行にまとまっていますが、代入と論理演算、否定が混ざっており、しばらく立ち止まって意味を解釈する必要があります。後者は行数は増えますが、処理の順序や意図が明確で理解しやすくなります。

筆者所感

私の経験では、短絡評価を使った表現が行をまたぐほど長くなる、丸括弧が多用されて読みづらくなる、あるいは評価の副作用(代入やリソース取得)に依存している場合は、分けて書くことが多いです。目安としては読み手が「一瞬で理解できる」なら短絡評価を使いますが、少しでも考えさせる要素があるなら分ける方が親切だと思います。

例:複雑なロジックと格闘する

著者見解

Range クラスの重なり判定を考えます。例えば [0, 5) と [3, 8) のような半開区間があったとき、2 つの Range が重なっているかどうかを判定する実装は次のようになりがちです。

bool OverlapsWith(Range other) {
    return (begin >= other.begin && begin < other.end) ||
           (end  <= other.begin && end <= other.end) ||
           (begin <= other.begin && end >= other.end);
}

しかし、このような複雑な論理式は読み手(自分も含め)にとって理解しにくく、正しさに自信を持ちづらいことが多いです。こういった場合は「正しい(重なっている)場合」を直接考えるのではなく、「重なっていない(=互いに範囲外にある)場合」を先に考えると簡潔になります。範囲が重ならないのは、片方の範囲がもう片方の前に完全にあるときだけです。これを使うと実装はずっと明快になります。

bool OverlapsWith(Range other) {
    if (other.end <= begin) return false; // other が左側に完全にある
    if (other.begin >= end) return false; // other が右側に完全にある
    return true;                            // それ以外は重なっている
}
筆者所感

この項目は単なるテクニックの羅列というより、「別の観点から考える」ことの重要性を教えてくれます。自分で書いたコードを読んで「直感的に理解できない」と感じたら、それは他人にも理解されにくい可能性が高いです。最近はAIがあるので、わかりにくいコードをAIに読ませてリファクタリングすることができるので、有効活用していくべきです。

巨大な文を分割する

著者見解

次のようなコードを見てみます。

const update_highlight = (message_num) => {
    if ($("#vote_value" + message_num).html() === "Up") {
        $("#thumbs_up" + message_num).addClass("hightlighted");
        $("#thumbs_down" + message_num).removeClass("hightlighted");
    } else if ($("#vote_value" + message_num).html() === "Down") {
        $("#thumbs_up" + message_num).removeClass("hightlighted");
        $("#thumbs_down" + message_num).addClass("hightlighted");
    } else {
        $("#thumbs_up" + message_num).removeClass("hightlighted");
        $("#thumbs_down" + message_num).removeClass("hightlighted");
    }
};

各式自体はそれほど大きくないものの、同じ長い式が繰り返されると全体が読みづらくなります。幸い同じ式(同じセレクタやクラス名)が複数回出現しているため、それらを変数に抜き出すことでコードを分割できます。これは DRY(Don't Repeat Yourself)原則の良い例です。

抽出した例を示します(クラス名の綴りは "highlighted" に修正しています)。

const update_highlight = (message_num) => {
    const thumbs_up = $("#thumbs_up" + message_num);
    const thumbs_down = $("#thumbs_down" + message_num);
    const vote_value = $("#vote_value" + message_num);
    const hi = "highlighted";
    if (vote_value.html() === "Up") {
        thumbs_up.addClass(hi);
        thumbs_down.removeClass(hi);
    } else if (vote_value.html() === "Down") {
        thumbs_up.removeClass(hi);
        thumbs_down.addClass(hi);
    } else {
        thumbs_up.removeClass(hi);
        thumbs_down.removeClass(hi);
    }
};

const hi = "highlighted" は厳密には必須ではありませんが、次の利点があります。

  • タイプミスが減る(クラス名の綴りミスを防げる)。
  • 行の横幅が短くなり可読性が向上する。
  • クラス名を変更する必要が生じた場合、一箇所だけ編集すれば済む。

また、セレクタを変数に入れることで同じ DOM クエリを繰り返さずに済み、パフォーマンス面や意図の明確化にも寄与します。

筆者所感

この項目で示された手法は、多くの人が普段から無意識に行っていることだと思います。実際に比較してみると、変数を抜き出したほうが圧倒的に読みやすくなります。私自身もより意識してこのような分割を徹底し、可読性と保守性を高めていきたいと考えています。

式を簡潔にするもう1つの創造的な方法

著者見解

次のように、同じパターンの式が繰り返されて長くなっている例を考えます。

void AddStats(const Stats& add_from, Stats* add_to) {
    add_to->set_total_memory(add_from.total_memory() + add_to->total_memory());
    add_to->set_free_memory(add_from.free_memory() + add_to->free_memory());
    add_to->set_swap_memory(add_from.swap_memory() + add_to->swap_memory());
    add_to->set_status_string(add_from.status_string() + add_to->status_string());
    add_to->set_num_processes(add_from.num_processes() + add_to->num_processes());
    ...
}

式は似通っていますがフィールド名が異なるため、単純なループや汎用関数に置き換えにくいケースです。C++ ではプリプロセッサマクロを利用して繰り返しを簡潔に表現することができます。例えば以下のように書けます。

void AddStats(const Stats& add_from, Stats* add_to) {
    #define ADD_FIELD(field) add_to->set_##field(add_from.field() + add_to->field());
    ADD_FIELD(total_memory);
    ADD_FIELD(free_memory);
    ADD_FIELD(swap_memory);
    ADD_FIELD(status_string);
    ADD_FIELD(num_processes);
    #undef ADD_FIELD
}

このようにすると各フィールドの処理が短くなり、可読性が向上する場合があります。

筆者所感

マクロは強力でコードを簡潔にできる反面、展開後のコードが見えにくくなり、バグが入り込みやすいという欠点もあります。私自身、普段扱う言語にマクロがないため多用することはありませんが、マクロが存在する言語を扱う際は慎重に使うべきだと考えています。

第9章 変数と読みやすさ

変数を削除する

著者見解

無意味に見える一時変数は削除すべきです。例えば次のような例を考えます。

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

この場合 now は式を分割して説明しているわけでもなく、datetime.datetime.now() をそのまま代入しても十分に明確です。しかも一度しか使われていないため、冗長な「残骸」に過ぎません。

同様に中間結果を保持するだけで無駄になっている変数もよく見られます。次は誤った(あるいは不適切な)例と改善例です。

// よくない例(index_to_remove を定義しているが、再代入可能にしないなど問題もある)
let index_to_remove = null;
for (let i = 0; i < array.length; i++) {
    if (array[i] === value_to_remove) {
        index_to_remove = i;
        break;
    }
}
if (index_to_remove !== null) {
    array.splice(index_to_remove, 1);
}

上記は中間変数を挟まずとも、見やすく簡潔に書けます。

// 改善例
for (let i = 0; i < array.length; i++) {
    if (array[i] === value_to_remove) {
        array.splice(i, 1);
        break;
    }
}

また、ループ内で制御のためだけに使われるフラグ変数(制御フロー変数)にも注意が必要です。例えば次のようなパターンです。

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

この done はプログラムのデータではなく制御のためだけに存在しています。多くの場合、break を使って直接ループを抜けるか、ネストが深くて break では対処しにくい場合は処理を別関数に切り出すことで、制御フロー変数を不要にできます。

while (/* 条件 */) {
    ...
    if (...) {
        break;
    }
}
筆者所感

著書内では「ループの途中から抜け出してはいけない」といった暗黙のルールを守ろうとしていると触れている箇所があり、筆者としてはそんなルールがあったのかと驚いたのですが、これは時代や文化、チームの慣習による違いが大きいのではないかと考えています。古いスタイルでは「単一の出口(single exit)」を重視して早期 return / break を避けることがありました。その意図は制御フローの一元化やリソース管理の容易さにあります。一方、現代的なベストプラクティスでは「可読性と意図の明確さ」を優先し、ガード節(早期 return)や breakcontinue を用いることが多く推奨されます。特にネストを浅くして処理の主旨を明確にするメリットは大きいです。

変数のスコープを縮める

著者見解

変数のスコープは可能なかぎり小さくするべきです。グローバル変数だけでなく、クラスのメンバ変数ですら「見える範囲」を狭めることで、同時に意識しなければならない状態を減らせます。スコープが小さくなるほど、変数の役割が限定され、理解しやすくなります。

例えば大きなクラスでメンバ変数を共有していると、その変数はクラス内の「ミニ・グローバル」になり、どのメソッドが値を変更するのか追いにくくなります。

class LargeClass {
    String str_;               // クラス全体から見える
    void method1() {
        str_ = ...;
        method2();
    }
    void method2() {
        // str_ を使用している
    }
    // 他にも str_ を見ないメソッドが多数ある
}

これをローカル変数に降格して、必要な部分に引数として渡すと、見える範囲を限定できます。

class LargeClass {
    void method1() {
        String str = ...;      // method1 のローカル変数
        method2(str);
    }
    void method2(String str) {
        // str を使用
    }
    // 他のメソッドからは str が見えない
}

また、static メソッドにすることでメンバ変数へのアクセスを不可能にしたり、クラスを責務ごとに分割したりすることでもスコープを縮められます。

筆者所感

変数のスコープを狭くすることはとても重要だと考えています。スコープが小さいとその変数に期待される役割が限定され、変数名も短くて済むことが多く、結果として可読性が向上します。static メソッドがメンバ変数にアクセスできない点を活用することでスコープを強制的に縮められる、という指摘はstatic メソッドの活用方法を広げてくれて、目から鱗でした。適切なクラスの分割やアクセス修飾子の設定を通じて、可能な限りスコープを狭くしていく工夫をしていきたいです。

C++のif文のスコープ

著者見解

if 文の条件部分で変数を定義すると、その変数のスコープが if ブロック内に限定されます。変数が if の外で使われる必要がない場合は、あえて条件内で定義することで「その変数はここだけで使われる」ということを明確にでき、読み手がいつ・どこでその変数を気にすべきかを減らせます。例を示します。

// 条件式の外で定義している例(if 文の後も info を意識する必要がある)
PaymentInfo* info = database.ReadPaymentInfo();
if (info) {
    cout << "User paid: " << info->amount() << endl;
}

// if 文の中で定義する例(info のスコープは if の中だけ)
if (PaymentInfo* info = database.ReadPaymentInfo()) {
    cout << "User paid: " << info->amount() << endl;
}

条件内で定義すれば、info が if の外側で誤って参照されることを防げますし、変数のライフタイムと責務がより限定され可読性が上がります。

筆者所感

どちらを採用するかは可読性とのトレードオフになると私は考えています。例えば、右辺の式が長く複雑であったり、代入の副作用がある場合は、条件内に書くと読みづらくなることがあります。読みやすさを優先するなら、あえて一行で代入せずに先に名前をつけてから if を書く方が良い場合もあります。

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

著者見解

次のようにグローバル変数を使った実装は、見た目には submit_form() だけで使われているように見えても、本当にそうかは分かりません。グローバルに露出していると他所から誤って参照・変更される危険があります。

// 問題のある例(グローバル変数を使用)
let submitted = false;

function submit_form(form_name) {
    if (submitted) {
        return;
    }
    ...
    submitted = true;
}

著者は、submitted をクロージャで囲ってスコープを限定するパターンを紹介しています。即時関数(IIFE)を使えば、submitted は外部から見えない「プライベート」変数になります。

// クロージャ(IIFE)でスコープを隠す例
const submit_form = (function () {
    let submitted = false;
    return function (form_name) {
        if (submitted) return;
        ...
        submitted = true;
    };
}());
筆者所感

JavaScript の歴史的な手法としてクロージャ(IIFE)でスコープを隠すのは有効ですが、現代の環境ではより読みやすく安全な選択肢が複数あります。実務では次のいずれかを優先することをおすすめします。

  • モジュールスコープ(ES6 モジュール)を利用する
    ファイル単位のスコープに変数を置けば、外部から見えなくなります。例えば module ファイル内で let submitted = false; として export しない限り外部からアクセスできません。

    // module submitForm.js
    let submitted = false;
    
    export function submitForm(formName) {
        if (submitted) return;
        ...
        submitted = true;
    }
    
  • クラスのプライベートフィールドを使う(ES2022 の #private)
    インスタンスごとに状態を持たせたい場合はプライベートフィールドが直感的で安全です。

    class FormHandler {
        #submitted = false;
    
        submit(formName) {
            if (this.#submitted) return;
            ...
            this.#submitted = true;
        }
    }
    
  • TypeScript の private/protected を利用する
    TypeScript ならコンパイル時にアクセス制御をチェックできます(実行時の完全な隠蔽とは異なりますが安全性は向上します)。

総括すると、古いパターン(グローバル変数/IIFE)も有効ですが、現在は ES6 モジュール、let/const、クラスのプライベートフィールド、TypeScript の型情報といった機能を活用することで、より明確で保守しやすい「プライベート」変数の管理ができます。

PythonとJavaScriptのネストしないスコープ

著者見解

Python や(従来の)JavaScript では、ブロック単位のスコープが存在せず、ブロック内で定義した変数は関数全体に影響します。そのため、あるブロック内でしか設定されない変数を、ブロックの外側で必ず使う場合は、先に最も近い共通の祖先で初期化しておくべきです。例を示します。

# 問題のある例(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)

上のコードでは、request が偽またはループ内で条件が満たされないと example_value が定義されず、最後の行で NameError になる可能性があります。安全にするには、共通の祖先で初期化しておきます。

# 改善例(明示的に初期化)
example_value = None
if request:
    for value in request.values:
        if value > 0:
            example_value = value
            break
if example_value is not None:
    for logger in debug.loggers:
        logger.log("Example: ", example_value)

同様の問題は JavaScript でも起こります(var は関数スコープなので特に注意)。ES6 の let / const を使えばブロックごとのスコープにできますが、やはり「参照する前に初期化されているか」を意識する必要があります。

// 改善例(モダン JS)
let exampleValue = null;
if (request) {
    for (const value of request.values) {
        if (value > 0) {
            exampleValue = value;
            break;
        }
    }
}
if (exampleValue !== null) {
    for (const logger of debug.loggers) {
        logger.log("Example: ", exampleValue);
    }
}
筆者所感

私がよく使う PHP にも同様の癖をよく見かけます。例えば try ブロック内で変数を初期化し、その変数を try の外で返すようなコードです。文法的には許されますが、変数のスコープが関数全体に広がるため読みづらくなることが多いです。一方で、ブロック内で初期化して、その後 return することで「変数を不変に保つ」設計もよく見られます。どちらの方針にも利点と欠点があるため、私は可読性・安全性・保守性の観点からそれぞれのメリット・デメリットを比較し、状況に応じてどちらを採用するか判断するようにしています。

定義の位置を下げる

著者見解

かつての C 言語では、関数やブロックの先頭で変数を宣言する必要がありました。しかし現代の多くの言語(C++ や Python、Java など)では、そのような制約はなく、変数は「使う直前」に定義して問題ありません。変数定義を遅らせるとスコープが狭くなり、読み手が意識すべき要素が減って可読性が向上します。

例として、変数を先にすべて並べたパターンと、使用直前に定義したパターンを比較します。

# 変数を先に定義した例
def view_filtered_replies(original_id):
    filtered_replies = []
    root_message = Messages.objects.get(original_id)
    all_replies = Messages.objects.select(root_id=original_id)
    root_message.view_count += 1
    root_message.last_view_time = datetime.datetime.now()
    root_message.save()
    for reply in all_replies:
        if reply.spam_votes <= MAX_SPAM_VOTES:
            filtered_replies.append(reply)
    return filtered_replies
# 変数定義を使用直前に移動した例(可読性が向上する)
def view_filtered_replies(original_id):
    root_message = Messages.objects.get(original_id)
    root_message.view_count += 1
    root_message.last_view_time = datetime.datetime.now()
    root_message.save()
    all_replies = Messages.objects.select(root_id=original_id)

    filtered_replies = []
    for reply in all_replies:
        if reply.spam_votes <= MAX_SPAM_VOTES:
            filtered_replies.append(reply)
    return filtered_replies
筆者所感

私自身はプログラミング経験が浅く、C 言語のように関数先頭で変数を宣言する必要があった昔の習慣を知らなかったため、年配の方のコードを読んでいると、関数先頭に変数が並んでいるのを見かけることがしばしばあり、なぜだろうと思っていました。例外処理や複数の分岐で同じ変数を共有する必要がある場合や、言語の仕様で前宣言が求められる場合など考慮が必要な場合もありますが、基本的には「使う直前で定義する」ことを基本方針としていきたいです。

変数は一度だけかきこむ

著者見解

変数への書き込み(代入)は原則として一度きりにすべきです。頻繁に再代入される変数は値の変遷を追うのが難しくなり、バグを見つけにくくなります。

筆者所感

私は基本的に、カウンタやループ変数など明らかに例外となる場合を除き、変数を再代入しない設計を好みます。再代入が必要になったら既存の変数を書き換えるのではなく、新しい変数名を用意して値を保持することで、過去の値や状態遷移を明確にできます。現代のほとんどの言語や実務環境では、変数を増やすことによるメモリコストは問題にならないため、可読性と保守性を優先して構いません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?