Skip to content
Published on

プログラミングが上達する方法 — 原理、習慣、そして深さ

Authors

はじめに: 上手いという言葉の重み

「プログラミングが上手くなりたい」と誰もが言います。しかし、いざ何が上手いことなのかと問われると、答えはぼやけます。ある人はアルゴリズムの問題を速く解くことを思い浮かべ、ある人は新しいフレームワークを速く習得することを思い浮かべます。また別の人は、画面に見栄えのする結果を速く出すことを思い浮かべます。

この記事は、そのぼやけた定義をはっきりさせるところから始まります。私は派手な裏技や「これさえ知れば大丈夫」といった約束はしません。そうした約束はたいてい嘘か、よくても半分しか真実ではありません。代わりに、長くこの仕事を続けてきた人たちが共通して持つ原理と習慣、そしてその習慣を育てる具体的な方法についてお話しします。

先に一つだけ申し上げておきます。上手いというのは才能の問題というより、積み重ねの問題です。ピーター・ノービッグが「Teach Yourself Programming in Ten Years」で指摘したように、深さは短い時間では作られません。しかし、同じ10年を過ごしても、積み重ねの質は人によって大きく異なります。この記事は、その質を高める方法についてのものです。

1. 上手いの定義: 動き、読め、保たれる

まず定義から合意しましょう。私はよく作られたコードを三つの層で見ます。

問い失敗したときの症状
動く意図したことを正確に行うかバグ、誤った結果、エッジケースの抜け
読める他人が理解できるかレビューの遅延、頻繁な質問、誤解
保たれる変更によく耐えるか小さな修正が連鎖的な故障を招く

初心者は最初の層しか見ません。動けば終わりだと考えるのです。しかし現場では、コードの寿命は長いものです。一度書いて捨てるコードはまれです。半年後の自分、一年後の同僚が、そのコードを再び読んで直します。だからこそ二番目と三番目の層が決定的です。

ブライアン・カーニハンの有名な言葉があります。デバッグはコードを書くことよりも二倍難しい。だから、できる限り賢くコードを書くと、定義上あなたはそれをデバッグできるほど賢くはない、という意味になります。上手いの定義に「謙虚さ」が入る理由がここにあります。

2. 基礎力: データ構造とシステムへの理解

基礎力という言葉は、しばしば面接用のアルゴリズムと誤解されます。しかし本当の基礎力は二つの感覚です。第一に、データがメモリ上でどう表現され、移動するのか。第二に、自分が書いたコードが実際の機械とネットワークの上でどう実行されるのか。

2.1 データ構造は選択の問題だ

データ構造を暗記することには意味がありません。重要なのは状況に合った選択です。次の例を見てください。リストから重複を取り除く二つの方法で、計算量がどう違うのか感覚としてつかめる必要があります。

# O(n^2) — リストの中で in 検査を繰り返す
def dedup_slow(items):
    result = []
    for x in items:
        if x not in result:   # 毎回、線形探索
            result.append(x)
    return result

# O(n) — 集合のハッシュ参照を活用する
def dedup_fast(items):
    seen = set()
    result = []
    for x in items:
        if x not in seen:     # 平均で定数時間
            seen.add(x)
            result.append(x)
    return result

二つの関数は同じ結果を出します。しかし入力が10万件になると、最初のものは事実上止まり、二番目のものはまばたきする間に終わります。基礎力とは、この違いをコードを見た瞬間に感じる力です。暗記した計算量の表ではなく、データがどこにどう収まるのかを思い浮かべる習慣です。

2.2 システムへの理解

Web開発者であれば、一つのHTTPリクエストがどんな経路をたどるのか描けるべきです。次は単純化した流れです。

ブラウザ
  -> DNS 照会 (ドメイン -> IP)
  -> TCP 接続 / TLS ハンドシェイク
  -> HTTP リクエスト送信
        -> ロードバランサ
        -> アプリケーションサーバ
        -> キャッシュ参照 (ヒットならここで応答)
        -> データベースクエリ
  <- 応答のシリアライズ (JSON など)
  <- レンダリング

この図を頭の中に持っていれば、「なぜ遅いのか」という問いの前で漠然としません。キャッシュを疑うか、クエリを疑うか、シリアライズを疑うか、候補を絞ることができます。システムの理解とは、結局のところ問題の位置を絞る地図です。

3. デバッグ: 推測ではなく観察

多くの人はデバッグを運だと考えます。コードをあちこち変えてみて、たまたま直れば幸いだ、と。しかし上手い人のデバッグは科学に近いものです。仮説を立て、観察で検証し、候補を半分ずつ減らしていきます。

3.1 デバッグの基本ループ

1. 再現する   — いつも同じ条件で同じ症状が出るようにする
2. 絞り込む   — 問題のある区間を半分ずつ切ってみる (二分探索)
3. 仮説を立てる — 「この変数が null だろう」のような検証可能な文
4. 観察する   — ログ、デバッガ、出力で仮説を確認する
5. 直す       — 原因を直し、症状ではなく原因が消えたかを見る
6. 防ぐ       — 同じ種類のバグを止めるテストやガードを残す

このループで最も頻繁に飛ばされるのは、一番目の再現です。再現できないバグを直そうとするのは、暗闇で刃を振るうようなものです。まず安定した再現手順を作ることに時間を使うのが、結局は一番速い道です。

3.2 二分探索式のデバッグ

長い処理過程のどこかで値が壊れるなら、真ん中に観察地点を一つ打ち込みます。

def process(records):
    cleaned = clean(records)
    # 観察地点: ここで値は正常か?
    assert all(r.get("id") for r in cleaned), "id が空だ"
    enriched = enrich(cleaned)
    return summarize(enriched)

もし assert が通れば問題は enrich より後にあり、失敗すれば clean にあります。一度の観察で候補の空間が半分になります。これが推測と科学の違いです。

4. 抽象化と単純さ: 少ないほど良い

抽象化は諸刃の剣です。良い抽象化は複雑さを隠して認知負荷を減らします。悪い抽象化は複雑さを移すだけで、そこにもう一層の間接性を加えます。

リッチ・ヒッキーは「Simple Made Easy」の講演で、単純さ(simple)と容易さ(easy)を区別しました。容易さは慣れの問題であり、単純さは絡み合いの問題です。一つのことだけを行うのが単純であり、手に馴染んでいるのが容易です。私たちはしばしば容易なものを追って、絡み合ったものを作ってしまいます。

4.1 早すぎる抽象化の罠

次はよくある間違いです。二か所で似たコードを見て、すぐに共通関数にまとめることです。

# 二か所で使われているという理由でまとめた関数
def handle(entity, kind):
    if kind == "user":
        validate_user(entity)
        save_user(entity)
    elif kind == "order":
        validate_order(entity)
        send_invoice(entity)
        save_order(entity)
    # kind が増えるたびに分岐が増え、二つの流れが互いを汚染する

表面的な類似に騙されてまとめると、時間が経つにつれて分岐が育ち、関数は誰のものでもない怪物になります。二度の繰り返しが見えたときは、いっそそのままにしておくほうが良いものです。三度目が現れたときに初めて、本当の共通点が何かが見えてきます。「早すぎる抽象化よりも少しの重複のほうが良い」という格言はここから来ます。

4.2 単純さの測り方

単純さは感覚ではなく、数えられるものです。一つの関数が扱う概念の数、受け取る引数の数、取りうる状態の数を数えてみてください。次の表は粗い信号です。

信号単純さの側複雑さの側
関数の引数の数3個以下6個以上
真偽値の引数なし動作を変えるフラグが複数
分岐の深さ2段以下4段以上の入れ子
一つの関数の責務一つ複数 (名前に and が入る)

名前に and が入るなら、その関数は二つに分けるべきという信号です。validateAndSave という名前を見たら、検証と保存は異なる理由で変わるものだということを思い出してください。

5. 読みやすいコード: コードは人のために書く

コンパイラはどんな変数名も受け入れます。変数名はもっぱら人のためのものです。だから良いコードは散文のように読めます。

5.1 名前が半分だ

# 悪い: 意図が名前にない
def f(d, n):
    return [x for x in d if x[1] > n]

# 良い: 名前がコードを説明する
def filter_above_threshold(records, threshold):
    return [r for r in records if r.score > threshold]

二つの関数の動作は同じですが、下の関数にはコメントが要りません。良い名前は最も安い文書です。そして名前は最初から完璧である必要はありません。意味がはっきりしたらすぐに直す習慣が大切です。

5.2 コメントは「なぜ」を書く

コード自体が「何を(what)」するのかは見せてくれます。コメントは「なぜ(why)」を書くべきです。

# 悪い: コードをそのまま書き写したコメント
i = i + 1  # i を 1 増やす

# 良い: コードだけでは分からない理由
# 外部 API は 0 からではなく 1 からページを数える
page = page + 1

「何を」を説明するコメントは、コードが変わると嘘になります。「なぜ」を説明するコメントは長く生き残り、未来の読者を救います。

6. テスト: 未来の自分のための保険

テストは面倒な義務ではなく、設計の道具です。テストしにくいコードは、たいてい設計の悪いコードです。依存が絡み合っているか、一つの関数があまりに多くのことをしているからです。

6.1 良いテストの形

# 対象の関数
def apply_discount(price, rate):
    if not 0 <= rate <= 1:
        raise ValueError("rate は 0 と 1 の間でなければならない")
    return round(price * (1 - rate), 2)

# テスト: 境界と例外を一緒に見る
def test_apply_discount():
    assert apply_discount(100, 0.0) == 100.0     # 割引なし
    assert apply_discount(100, 0.2) == 80.0      # 通常の場合
    assert apply_discount(100, 1.0) == 0.0       # 全額割引の境界

    import pytest
    with pytest.raises(ValueError):
        apply_discount(100, 1.5)                  # 不正な入力

良いテストは正常な場合だけを見ません。境界値と不正な入力を一緒に見ます。バグはたいてい、その縁で育つからです。

6.2 テストピラミッド

        /\
       /  \      E2E テスト (少なく)   — 遅いが現実に近い
      /----\
     /      \    結合テスト (ほどほど) — コンポーネント間の連結を見る
    /--------\
   /          \  単体テスト (多く)     — 速く正確に絞る
  /------------\

速く狭く突く単体テストを土台に多く置き、遅いが現実に近いE2Eテストを頂上に少しだけ置きます。この比率が逆さまになると、テスト群は遅くなり壊れやすくなります。

7. 漸進的な改善: キャンプ場の規則

ケント・ベックの言葉を借りれば、変更を簡単にしてから簡単な変更をせよ、という原則があります。大きなリファクタリングを一度に押し込まず、手の触れた所を少しずつ良くして出てくるのです。ボーイスカウトの規則のように、最初に来たときよりも少しきれいにして去るのです。

マーティン・ファウラーの「Refactoring」が教える核心は、小さな一歩です。テストで安全網を張った後、一度に一つずつ、動作を変えずに構造だけを直します。大きな決断の英雄的な書き直しよりも、毎日の小さな整理がコードベースを生かします。

悪いパターン:  6か月の放置 -> 巨大な書き直し -> 新しいバグの爆弾
良いパターン:  コミットごとに周りを少し整理 -> 負債が積もらない

8. 意図的な練習: ただ多くやっても上達しない

10年働いても上達しない人がいて、3年で深まる人がいます。違いは意図的な練習です。アンダース・エリクソンの研究が示すように、単なる反復は実力を停滞させます。自分の能力の縁で、即座のフィードバックを受けながら行う練習だけが実力を伸ばします。

8.1 心地よい反復を警戒せよ

すでにできることをまたやるのは休息であって練習ではありません。次は意図的な練習の具体的な形です。

  • 慣れた言語ではなく、考え方が異なる言語を一つ選んで小さなプロジェクトを作ってみます。オブジェクト指向だけやってきたなら関数型を、動的型付けだけなら静的型付けを。
  • 普段は使うだけの抽象を、一度は自分で底から実装してみます。小さなキーバリューストア、ミニルータ、単純な仮想マシンのようなものを。
  • 自分が書いたコードを一週間後にもう一度読み、何が理解を妨げたのかを書き留めます。それが次に避けるパターンです。

8.2 フィードバックループを短く

練習の効果はフィードバックの速さに比例します。保存するとすぐにテストが走り、型検査器が即座に誤りを指摘してくれる環境を作ってください。フィードバックが速いほど、試行と修正の回転が速くなり、速い回転がそのまま速い成長です。

9. コードを読む: 書く前にまず読め

開発者は自分が書く時間よりも、他人のコードを読む時間のほうがはるかに長いものです。それなのに、ほとんどの人は読むことを別に練習しません。よく書く人は、ほぼ例外なくよく読む人です。

9.1 読むための戦略

1. 入口を探す     — main、ルートハンドラ、エントリポイントから
2. 大きな塊を見る — ディレクトリ構造とモジュール境界をまず把握
3. 一つの流れを追う — リクエスト一つが最後まで行く経路を追跡
4. 質問を書き留める — 「これはなぜこうしたのか?」を記録
5. 仮説を検証する — 小さな修正をしてテストで確認

良いオープンソースを一つ選び、一つの機能の流れを最後まで追ってみてください。リナックスカーネルのように大げさである必要はありません。自分が毎日使うライブラリの中核関数一つで十分です。よく書かれたコードを十分に読めば、良いコードの感覚が指先に染み込みます。

9.2 読むことから学ぶもの

他人のコードを読みながら、私たちは単に動作を理解するだけにとどまりません。その人がどんなトレードオフを選んだのか、どんな場合に前もって備えたのか、どんな名前で意図を表したのかを吸収します。文章を上達させるには良い文章を多く読まねばならないように、コードもそうです。

10. AI支援時代の力量: 審美眼と検証

いまや道具がコードを代わりに書いてくれる時代です。この変化は基礎力を無意味にしません。むしろ二つの力量をより重要にします。審美眼と検証です。

10.1 審美眼: 良い答えを見分ける目

生成されたコードがもっともらしく見えることと、正しいことは別です。次のコードを見てください。道具がよく出す類のコードです。

# もっともらしいが危険なコード
def get_user(users, user_id):
    return [u for u in users if u["id"] == user_id][0]

動作はします。しかし user_id がなければ IndexError で落ち、重複があれば静かに最初のものだけを選びます。審美眼のある人はこの隙間をすぐに見ます。

# 意図を明確にしたコード
def get_user(users, user_id):
    matches = [u for u in users if u["id"] == user_id]
    if not matches:
        raise KeyError(f"ユーザーが見つからない: {user_id}")
    if len(matches) > 1:
        raise ValueError(f"重複したユーザー id: {user_id}")
    return matches[0]

生成道具は平均的なコードを速くくれます。平均を正しいものへと引き上げるのは、依然として人の審美眼です。そしてその審美眼は、前の章で述べた基礎力から生まれます。

10.2 検証: 信じるな、確かめよ

生成されたコードは自信満々に間違えます。だから検証が核心の技術になります。テストで確認し、小さな入力で自分で動かしてみて、境界条件を吟味すること。これらを道具は代わりにやってくれません。道具が速くなるほど、その結果に責任を持って検証する人の価値はむしろ上がります。

道具が得意なこと人が責任を持つこと
ありふれたコードを速く生成問題を正しく定義すること
慣れたパターンを適用トレードオフを判断すること
ボイラープレートを書く結果を検証し責任を持つこと
文法とAPIを記憶単純さと審美眼を保つこと

11. アンチパターン: 上手い人が避けること

成長は良いものを足すことでもありますが、悪いものを引くことでもあります。次はよくあるが高くつくアンチパターンです。

  • コピー&ペーストのプログラミング: 理解せずに持ってきたコードは、いつか必ず請求書を送ってきます。
  • 万一のための一般化: まだ来ていない要求のために前もって抽象化すると、たいていその要求は来ず、複雑さだけが残ります。
  • 英雄的なデバッグ: ログもテストもなく頭の中だけで追跡する自慢。再現と観察の抜けたデバッグは運に頼ります。
  • 沈黙する失敗: 例外を飲み込んで空の値を返すと、問題は消えずにより遠くへ隠れます。
  • 賢さの誇示: 一行に圧縮した賢いコードは、書いた本人だけを満足させ読者を苦しめます。

このリストの共通点は、いずれも短期的には楽で、長期的には高くつくということです。上手い人はそのずれを知っています。

12. 成長のロードマップ: 段階別の地図

最後に、やや粗いですが役に立つ段階別の地図を描いてみます。この段階は役職ではなく、思考の幅についてのものです。

段階1: 動かす
  - 文法と道具に慣れる
  - 小さなプログラムを最後まで完成させてみる
  - エラーメッセージを読む習慣をつける

段階2: よく動かす
  - データ構造の選択の差を感じる
  - デバッグを推測ではなく観察で行う
  - テストを自分で書く

段階3: 他人に読ませる
  - 名前と構造で意図を表す
  - 小さな単位で漸進的に改善する
  - コードレビューをやり取りして学ぶ

段階4: システムとして考える
  - 単純さと抽象化を意識的に扱う
  - トレードオフを言葉で説明できる
  - 変更の費用を前もって見通す

段階5: 他人を育てる
  - 自分の判断基準を文章とレビューで伝える
  - チームのコードベースをより単純にする
  - 道具と人の役割を設計する

この地図で重要なのは、速い通過ではありません。各段階に十分とどまり、その感覚を手に馴染ませることです。上の段階に上がっても、下の段階の基礎力は消えず、土台になります。

おわりに: 長く続く人の習慣

プログラミングが上手くなる方法に秘密はありません。動き、読め、保たれるコードを目標に据え、データとシステムへの感覚を育て、推測ではなく観察でデバッグし、単純さを意識的に守り、人のためにコードを書き、テストで未来を守り、毎日少しずつ改善し、自分の能力の縁で意図的に練習し、他人のコードをまめに読むこと。道具がどれほど速くなっても、これらの習慣が生み出す審美眼と検証の力は、人の取り分として残ります。

結局、上手い人とは非凡な一撃を持つ人ではなく、平凡で良い習慣を長く守った人です。ノービッグの言う10年は長く感じられますが、毎日の小さな選択が積み重なれば、その道は思うより速く深まります。今日書く一つの関数の名前を、もう少し正直につけるところから。そこから始めればよいのです。

参考資料