仕事や生活で得た知見をなんとかアウトプットする場です

ころがるおもち

プログラミング初心者がリーダブルコードで習得すべき技術

現在、プログラミングの習得は、無料のwebサービス、スクール、有料動画によるe-ラーニングでどこでも学べる環境が手に入ります。

ただ、ひと通りやっても果たして実務で通用するのかな?と不安に思う人も多いですよね。

この記事では、とりあえずプログラミング学習を始めてなんとなく書けるようになったけど、初心者レベルを抜けられない人向けに現場で役立つプログラミングテクニックを名著『リーダブルコード』から厳選して紹介します。プログラミング言語によらず、少なくともオブジェクト指向言語では共通して廃れず役立つ知識です。

この記事で得られるメリット

  • 客観的に見て分かりやすいコードが書けるようになります
  • 独りよがりでない、現場のメンバーにも理解しやすいコードが書けるようになります

注意ポイント

プログラミング入門者で急いでスキル習得したい場合は、迷わずスクールに行ってください。
独学もよいですが、開発環境構築、デバッグ環境、エラーの解読にかなり時間も労力も消費します。
本来やりたかったことに到達する前に、何が起きているのか分からないことが連続し、結果、挫折してしまうのが独学の失敗王道パターンです。

プログラミングコードは、『読みやすさ』が最重要!

書籍『リーダブルコード』では、読みやすいコードをテーマにさまざまなテクニックを紹介しています。

読みやすいとはどういうことかというと、他人が読んでも理解しやすいということです。

さらに掘り下げると、個人的には、理解しやすいとは以下の要素で構成されています。

  • 他人が読んでも分かること
  • 考える範囲が小さいこと

ここでの他人とは、「過去の自分」も含みますよ?
プログラミングあるあるなんですが、過去に書いたコードを2週間後に見てみましょう。
誰が書いたのか分からないコードになっています。
これは、プログラミングあるあるです。

以下で出てくるコードはiOS開発で主に使用するSwiftを使って解説しています。

1. 関数から早く抜ける(早期リターン)

以下のような関数があるとします。

変更前

func contains(a: String, b: String) -> Bool {
    var contains: Bool = false
    
    if a.isEmpty || b.isEmpty {
        contains = false
    }
    
    if b.contains("test") {
        contains = true
    }
    
    return contains
}

いにしえのプログラミングでは、関数では複数のreturnを使ってはいけないと言われていました。
わたしもとある現場のシニアエンジニアの方からそう教わった記憶があります。
returnの位置が複数あるとどこでreturnされるか分からなくなるから、という理由だっと記憶しています。

それが、リーダブルコードの書き方だとこうなります。

変更後

func contains(a: String, b: String) -> Bool {
    // ①
    if a.isEmpty || b.isEmpty {
        return false
    }
    // ②
    if b.contains("test") {
        return true
    }
    // ③
    return false
}

お分かりでしょうか?

各分岐判定で正であれば、そこでreturnするようにしています。

説明の便宜上、コードのブロックに番号①〜③を振りました。

ここで重要なことは、①の判定処理が正だった場合、②以降はもう見なくても良いということなんです。

これが、考える範囲が小さいことを実現していますよね。

今すぐに、しかも簡単にコードを改善できるテクニックの1つです。

2. ネストを浅くする

ネストとは、入れ子のことです。これを少なくすると読みやすいコードになります。

深いネストのイメージとは、以下のような制御構造のコードですね。
いくつものif-else文が入れ子構造になっています。実際はこの「...」に処理が書かれるのでこれはもう読んでて分からないですよ。

if xxx {
    // ...
    if xxx {
        // ...
        if xxx {
            // ...
            if xxx {
                // ...
            } else {
                // ...
            }
        } else {
            // ...
        }
    } else {
        // ...
    }
} else {
    // ...
}

具体的なコードの改善例を見てみます。

ユーザーの結果や許可できたかどうかの結果を判別してそれぞれの条件で異なる処理をする例ですね。

変更前

if user.result == SUCCESS {
    if permission.result != SUCCESS {
        reply.writeError(permission.result)
        reply.done()
        return
    }
    reply.writeError("")
} else {
    reply.writeError(user.result)
}
reply.done()

ifがあって、その中にifがある。ネストの深さがレベル2の状態です。
しかも条件が成功したかと、成功してなかったらというややこしいケース。
まぁそういうケースが珍しいわけじゃないのでいいとして、else句もあるからちょっと読んでてしんどくなってきますね。

変更後

// ①permissionのことを考えなくていい、ラク!
if user.result != SUCCESS {
    reply.writeError(user.result)
    reply.done()
    return
}

// ②ユーザーの結果は成功したという前提で考える
if permission.result != SUCCESS {
    reply.writeError(permission.result)
    reply.done()
    return
}

// ③ユーザーも許可もどちらも成功!
replay.writeError("")
reply.done()

ご覧の通り、すっきりしましたね!
ネストの深さが小さくなり、if文が1階層だけになっています。

コメント①〜③は便宜上追加してますが、それを除くとコード行数は1行増えてます。
増えているけど、だいぶ見やすくなったと思いませんか?

上述のコードのコメントの通り、①についてはpermissionについては考えなくて良いようにコードが改善されていますね。

ここでは、1で紹介した早期リターンも活用しています。

これも考える範囲を小さくしている結果の表れですね。

3. 変数のスコープを小さくする

変数が生存する範囲が狭いほど、コードが見やすくなります。

変更前

final class SampleTest {
    private var tststr: String = ""
    
    func method1() {
        tststr = "test"
        method2()
    }
    
    private func method2() {
        // tststrを使用
    }

    // 他の関数ではtststr使っていない
}

tststrはSampleTestクラスで使用できる文字型のインスタンス変数です。
上述のサンプロコードを見ると、このtststrはmethod1とmethod2でしか使っていないことが分かります。

他の関数でもtststrは使用されてないから、インスタンス変数である必要なくない?
じゃあ、ローカル変数に格下げして変数のスコープ小さくしようか!
その方が、他の関数ではtststrを使用していないことが分かりやすくなるからね。

というモチベーションで改善したコードが以下です。

変更後

final class SampleTest {
    func method1() {
        let tststr = "test"
        method2(tststr: tststr)
    }
    
    private func method2(tststr: String) {
        // tststrを使用
    }
}

tststrはmethod1の中でローカル変数として宣言され、method2の引数として渡すように修正されています。

これで、tststrはmethod1とmethod2で使われるだけで他の関数には影響ないんだなと考えることが小さくなりましたね。

4. 一度に一つのタスクを行う

クラスの粒度によりますが、関数は1つのタスクを実行するように作ると何をしているか分かりやすくなります。

コードの実装に集中しすぎて気づいたら1つの関数の中に10数行以上コードを書いていた、ということありませんか?

誰しも一度はあると思いますが、おそらく2つ以上のタスクを行っている可能性が高いです。

具体例で説明していきます。

ここにあるニュース記事があり、それに対して「いいね」「よくないね」ボタンがあるとします。
ニュース記事はスコアを保持していて、「いいね」で1点スコアが増え、「よくないね」で−1点がスコアが減ります。
また、「いいね」を取り消し(−1点)、「よくないね」の取り消し(+1点)もできます。
うっかり、「いいね」ボタン押してもう一度「いいね」を押して取り消すことあります。

このニュース記事のスコアを更新するタスクを見ていきます。

変更前

final class Article {
    
    enum VoteType: Int {
        case good = 1
        case bad = -1
        case none = 0
    }
    
    var score: Int = 0
    
    func changeScore(oldVote: VoteType, newVote: VoteType ) {

        guard newVote != oldVote else { return }
        
        switch newVote {
        case .good:
            score += (oldVote == .bad) ? 2 : 1     // ①
        case .bad:
            score -= (oldVote == .good) ? 2 : 1    // ②
        case .none:
            score += (oldVote == .good) ? -1 : 1   // ③
        }
    }
}

えー、実際書籍を見てサンプルコードを書いているのですが、結構ロジックが分からなくて戸惑いました。。

chageScore関数では、oldVote、つまり、更新前の投票状態、newVote、つまり、更新後の投票状態を表す引数として定義しています。

上記の①〜③のnewVote, oldVateの状態は以下のようになります。

  1. newVoteが「いいね」の場合、oldVoteは「よくないね」か「無投票」
  2. newVoteが「よくないね」の場合、oldVoteは「いいね」か「無投票」
  3. newVoteが「無投票」の場合、oldVoteは「いいね」か「無投票」

「更新前の投票状態」と「更新後の投票状態」のスコア更新処理を同時に行っているので、スコアを更新する数値をよく見て、あぁ、たしかに、そうなるなとやや考えながらコードの読解するので理解しにくいコードになってますね。

なので、「更新前の投票状態」と「更新後の投票状態」のスコア更新処理をそれぞれ行うように修正します。

変更後

final class Article {
    
    enum VoteType: Int {
        case good = 1
        case bad = -1
        case none = 0
    }
    
    var score: Int = 0
    
    func changeScore(oldVote: VoteType, newVote: VoteType ) {
        score -= oldVote.rawValue  // 更新前投票状態のスコアを取り消し
        score += newVote.rawValue  // 更新後の投票状態のスコアを反映
    }
}

めちゃくちゃシンプルになった!

5. 適切なコメントをする、もしくはコメントしない

なにをコメントするか、しないか指針がないと迷ういますよね。

よく言われたのは、コメントではなくコードを読んで理解できるのであれば、コメントは不要という助言です。

これはたしかにその通りで、以下のように分かりきってることにコメントしても無意味です。

// スコアを変更する
func changeScore(oldVote: VoteType, newVote: VoteType ) {
    // スコアを宣言する
    var score: Int       
    // ...
}

必要のないコメント

  • コードそのまんまを説明している
  • 読んでも理解が難しいコード(→つまり、コメントするよりコード修正しろと言う話)

書いたほうが良いコメント

  • 難しい処理のコード群の要約(要約を読んで時短!)
  • 既知の不具合コードの解説(どういう不具合か「自分」が後で見ても分かるように)
  • 後で実装する予定(Todo)

6. 明確な名前をつける

例えばこんな感じ。

count → articleCount

countだけだとなんのカウントか分かりませんよね。
記事数を意味するarticleCountに変更しています。

次はこれ。

func getPage(url: URL) {
    // do something
}

URLを使ってページを取得するような関数です。
しかし、どこから取得するんだろう。
ローカルデータから?データベースから?ネットから?getではどこからか分からないんですね。

まぁ実際はクラスのそれらしい命名すると思うのであまり気にならないかもしれませんが、インターネットから取得する場合は、

func fetchPage(url: URL) {
    // do something
}

// もしくは

func downloadPage(url: URL) {
    // do something
}

fetchかdownloadを使うと明確です。

関数もそうですが、クラスやローカル変数も含め、命名は長すぎても他の人が読んでも分かりやすいことが理想です!

しかし、、、

はっきり言って、命名はいつまで経っても難しいです。

毎回悩みます。

理由として、3つぐらいあると思ってます。

クラスや変数、関数への命名方法が難しい3つの理由

  1. 適切な英語名が出てこない
  2. ドメイン知識を正しく把握していない
  3. メンバー間での言葉のニュアンスが微妙に異なる

1. 適切な英語名が出てこない

プログラミングは基本的に英語で書かれていることがほとんどなので、クラスや関数名を命名するとき英語で表現することが慣習としてあります。適切な英語名が出てこないのは語彙力不足が原因の1つです。

なので、開発で使用する言語の標準APIを参照すると良いです。似たようなクラス名や関数名があれば、それらを参考にして命名できます。

2. ドメイン知識を正しく把握していない

ドメイン知識とは、開発しようとしている領域の専門用語だと思ってもらればよいです。

単にドメイン知識が足りなくて命名に悩む場合は、ドメイン知識増やせば良いだけの話です。
問題は、専門用語が英語に相当する表現がない場合です。その場合、プログラマーからは嫌がられますが、最悪ローマ字読みの表現でも良いです。
なぜなら、無理くり英語に変換されたネーミングを見ても何をするのかイマイチ分からないことが多い。
ローマ字読みは、最悪の場合ですよ。

3. メンバー間での言葉のニュアンスが微妙に異なる

これが命名を難しくさせている要因だと個人的には思ってます。

自分がこれだ!と思う言葉で命名したとしても、他のメンバーから見たらしっくり来てないこともまああります。

なぜこんな事が起きるかというと、自分と他人はそれぞれ生きてきた環境や背景が違うので使う言葉に定義にお互い差異があるんですよね。

そのため、メンバー全員が納得する正しい命名は存在しません。というかできないです。

あまりにも意味がかけ離れた命名はNGですが、7〜8割でお互いに納得する命名であればOKとしたほうが良いです。

でなればいつまで経っても命名できず時間を浪費して、開発が納期に間に合わないという本末転倒なことは避けましょう。

この命名、正しく明確な名前をつけるテクニックですが、『リーダブルコード』の一番最初に扱うテクニックとして紹介されています。

それほど重要だということです。

この命名の重要性は、個人的には強力な手法だと思っているドメイン駆動設計にも大いに役立つのでぜひ身に付けておきましょう。

7. コードを書かないこと

え??

どういうことってなりますよね。

コードは、要件から決めた仕様を実現するためにプログラマーが書いて実装します。

しかし、ですよ。

そもそも、その仕様は本当に必要ですか?

いや、コード書けば実現できますよ。でも、ユーザーの問題解決に必要なければ実装する必要ないですよね?

これは実装するのが面倒くさくて言っているのではありません。

システム開発というものは、そもそもお金がかかるんですね。開発者だけでなく、テスターの人件費もかかります。

なので、システムではなく、業務改善や仕組みを導入することで本来やりたかった問題解決がコスト少なくより速く実現できるかもしれないんです。

それを考えました?という意味での「コードは書かない」ということなんです。

ここで紹介したテクニックのうち、これだけはコードを書く前のテクニックです。

そして、『リーダブルコード』の機能実装に関する最後の13章で紹介されているテクニックでもあります。(14章以降はテストや演習を扱うのでここでは省略)

なかなか奥深いですよね、正直言うと非エンジニアの方から見ると、プログラマーはコード書くのが好きとか、コード書くだけの人と勘違いしていることが多いです。
一方で、プログラマー自体ももユーザーの課題検討よりコード書くのが楽しいとか思っている人も少なくありません。

勘違いしてほしくないのは、プログラミングもシステム開発もユーザーの課題を解決するツールの1つに過ぎないということです。

いかにお金をかけないでユーザーの問題解決に時間や労力を費やすか。

これに注力したほうが良いです。システムが登場するのは最後の方だと考えておいたほうが良いです。

そのコードを書くことで実現する要件は、誰の何を解決するのですか?そして、他に安くてまたは速く解決する手段はありませんか?