LoginSignup
78
68

More than 1 year has passed since last update.

可読性について Part3(コードの再構築)

Posted at

概要

  • ペーペーエンジニアの覚書
  • リーダブルコードのPart3のまとめ

前回まとめ

  • コードを大きく変更する技法
    • 「無関係の下位問題」を抽出する
    • 一度に1つのことをする
    • コードを言葉で説明する
    • 短いコードを書く

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

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

まず最初に「無関係の下位問題を抽出する」についてです。要するに単一責任の原則で、1メソッド1責任で命名以上のことをしないように、メソッドがファットにならないようにです。

もし命名以上の処理をしていたらそれは、別メソッドで切り出すべきです。

実例 其の壱

// 無関係の会問題を抽出する(原文まま)
// 与えられた経緯緯度に最も近いarrayの要素を返す
var findClosestLocation = function (lat, lng, array) {
  var closest;
  var closesDist = Number.MAX_VALUE;
  for(var i= 0; i < array.length; i += 1) {
    //2つの地点をラジアンに変換する(ここが下位問題に相当する)
    var latRad = radians(lat);
    var lngRad = radians(lng);
    var latRad2 = radians(array[i].latitude);
    var lngRad2 = radians(array[i].longitude);

    // 「球面三角法の第二余弦定理」の公式を使う
    var dist = Math.acos(Math.sin(latRad) * Math.sin(latRad2) +
               Math.cos(latRad) * Math.cos(latRad2) *
               Math.cos(lngRad2 - lhngRad));

    if (dist < closestDist) {
        closest = array[i];
        closestDist = dist;
    }
  }
  return closest;
}

上記コードは原文ままのメソッドになります。

メソッドで行われていることといたしましては、与えられた緯度経度に最も近いarrayの要素を返すというというメソッドなのですが、それに対して、「2つの地点をラジアンに変換する」というタスクが混ざっており責務が2つ発生してしまっています。

こちらを別メソッドとして抽出することができます。

// 新しい関数へ抽出
var sphericalDistance = function (lat1, lng, lat2, lng2) {
    // 2つの地点をラジアンに変換する
    var latRad = radians(lat1);
    var lngRad = radians(lng1);
    var latRad2 = radians(lat2);
    var lngRad2 = radians(lng2);

    //「球面三角法の第二余弦定理」の公式を使う
    return Math.acos(Math.sin(latRad) * Math.sin(latRad2) +
            Math.cos(latRad) * Math.cos(latRand2) *
            Math.cos(lngRad2 - lngRad));
}
// 残ったコード
var findClosestLocation = function (lat, lng, array) {
    var closest;
    var closestDist = Number.MAX_VALUE;
    for(var i=0; i < array.length; i += 1) {
        var dist =sphericalDistance(lat, lng, array[i].length, array[i].longitude)
        if(dist < closestDist) {
            closest = array[i];
            closestDist = dist;
        }
    }
    return closest;
}

1つめが新しくメソッドとして切り出したもの、2つめが残ったコードになります。

コードを別メソッドで切り出し、残ったコードは本来の責務1つのみとなります。

こうすることで、可読性も高まり修正をする時も、このメソッドは一つの理由だけで修正されることになります。

メソッドは小さくて独立したものになっていれば、機能追加・読みやすさの向上につながります。

これらを抽出するポイントとしては、メソッドの事前処理や事後処理などの本質とは関係のない処理、いわゆるグルーコードを積極的に抽出することです。

また抽出したメソッドが汎用性の高い場合は、utilsなどへ置いてプロジェクト内部で読み込めるようにしておくと便利です。

やりすぎ禁物

  • 無意味で小さな関数を作りすぎると、逆に読みにくくなってしまう

一度に1つのことを

  • 関数は1度に1つのことを行うべき
  • 手順
    • コードが行っているタスクを全て列挙する
    • タスクをできるだけ異なる関数に分割する

続いては「一度に1つの事を」です。一度に複数のことをするコードは理解しにくく、責務が複数あるとテストも書きづらくなってしまいます。

大きな関数は責務を分けて小さく分割すべきです。

では具体的な手順ですが、コードが行っているタスクを全て列挙して、それらをできるだけ異なる関数に分割していきます。

この「タスク」という定義はケースバイケースになると思うので、適宜判断しましょう。

一つの基準として関数の命名に悩むようであれば、「複数のことをしている関数だ」といえますので一つの基準として覚えておきましょう。

タスクは小さくできる

// タスクは小さくできる(原文まま)
var vote_changed = function (old_vote, new_note) {
    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);
}
// タスクは小さくできる(原文まま)
var vote_changed = function (old_vote, new_note) {
    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);
}

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);
}

こちらは例題として「賛成、upで+1」と「反対、downで-1」をユーザーが投票できる1つめのメソッドがあったとします。1つめのコードはスコアを更新するという1つのタスク以外にも「old_voteとnew_voteを数値にパースする」というタスクが含まれています。

これらのタスクを別々に切り出したコードが2つめになります。最小のタスクに切り出したことで可読性が増し、コードを「楽に理解できる」ようになったと言えます。

コードを言葉で説明する

おばあちゃんがわかるように説明できなければ、本当に理解したとは言えない。
(アルバート・アインシュタイン)

続いては「コードを言葉で説明する」です。コードの動作を説明する時に、自分よりも知識が少ない人でも「誰にでもわかるように」簡単な言葉で説明できないと本当に理解してコードを書いているとは言えません。

それには自分の考えを凝縮して、最も大切な概念にすることが必要です。

そして、普段からソースコードに触れているプログラマにとっては、コードそのものがプログラムの動作を説明する大切な手段になります。つまり、コードも「簡単な言葉」で書くべきです。

コードをより明確にする手順を使っていきます。

コードをより明確にする手順

  1. コードの動作を簡単な言葉で同僚にもわかるように説明する
  2. その説明の中で使っているキーワードやフレーズに注目する
  3. その説明に合わせてコードを書zく

ロジックを明確に説明する

  • 動作や仕様を言語化できない = 明確に理解できていない
  • 「簡単な言葉で説明する」手法は開発以外にも使用できる
  • 今回取り上げきれなかった解決例もあったのでぜひ書籍も

動作や設計/仕様をうまく言葉でできないのであれば何かを見落としているか、そもそも詳細が明確になっていないと言えます。

また、「簡単な言葉で説明する」手法は、開発以外でも適用できて、例えばデバッグの際にパソコンの横にアヒルのおもちゃを置いて、そのおもちゃに向かって語りかけることで、問題を解決するとう「ラバーダッキング」技法というものが存在します。

あと、リーダブルコードには先ほどの例よりももっと巨大なコードを改善する例があったのですが、こちらでは取り上げれるような大きさではなかったため、ぜひ書籍を読んでみて頂ければと思います。

それと、これらロジックの整理ということでは、サンプルのように実際にやりたいことを先に言語化してみるのと、個人的にはテスト技法のデシジョンテーブルや直交表、状態遷移表のNスイッチカバレッジを活用すれば、ロジックを整理するのにはとても有効だと感じました。テスト技法については今回は省きます。

短いコードを書く

  • 最も読みやすいコードは、何も書かれていないコードである
  • 実現するには、要求を正しく理解していること
  • 未使用のコードは削除する
  • 身近なライブラリに親しむ

続いては「短いコードを書く」です。このタイトルだけだとワンライン=正義のような誤解を生みそうなので、そうではないことを先に申し上げておきます。

リーダブルコードで一貫していることは「読みやすいコードが優れたコード」であるということです。

ではここで言われている「短いコードを書く」とはなんなのか?ですが、
リーダブルコードでは「最も読みやすいコードは、何も書かれていないコードである」と記述されています。

これはつまり「YAGNIの原則」にあたり、「後で使うだろうという予測の元に作ったものは、実際には10%程度しか使われないので、それに費やした時間の90%は無駄になる」という原則です。

そしてコードがないのであればバグが生まれることもありません。

ただし、YAGNIの原則を実現させるにはクライアントの要求を正しく理解している必要があります。

「何を実現させたいのか?何を解決させたいのか?」をしっかり把握することが「短いコードを書く」ことの第一歩になります。

また、規模が大きくなるにつれて、未使用のコードが生まれてきてしまいますが、これはあえて言うまでもなく削除していきましょう。

そして、ライブラリの活用も有効です。

これに関しては自作していった方がブラックボックス化されなくて良い、という意見もありそうですが、ライブラリの機能を熟知して実際に活用するのは有効な手段であると記載されています。

理由としては、成熟したライブラリのコードの裏側には、膨大な設計・デバッグ・修正・ドキュメント・最適化・テストが存在しており、現在まで生き延びてきたライブラリコードには大きな価値があるため、それを再利用すると時間とコード記述の節約になるためであると書かれています。

よく使われる統計では、平均的なエンジニアが1日に書く出荷用のコードは10行である」という統計があり、このことを考えるとライブラリの再利用の意義がわかる気がします。

まとめ

  • 「無関係の下位問題」を抽出する
    • メソッドと関係のない処理を抽出する
  • 一度に1つのことをする/手順
    • コードが行っているタスクを全て列挙する
    • タスクをできるだけ異なる関数に分割する
  • コードを言葉で説明する
    • コードの動作を簡単な言葉で説明できるように
  • 短いコードを書く
    • 将来使うだろうは10%しか使われない
    • 未使用のコードは削除
    • ライブラリの活用
78
68
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
78
68