Skip to content

良いコードを書くために

良いコードとは、将来の自分と他者が安全に変更でき、意図と根拠を追跡できる形に整えられたコードである。良さは美学ではなく、変更コストと不具合確率を下げるための工学的な性質である。

参考ドキュメント

  1. ISO/IEC 25010 quality model(品質特性の整理)
    https://iso25000.com/en/iso-25000-standards/iso-25010
  2. Google Engineering Practices: The Standard of Code Review(レビューの合格基準)
    https://google.github.io/eng-practices/review/reviewer/standard.html
  3. IPA, ESCR Ver. 3.0 組込みソフトウェア向けコーディング作法ガイド(日本語)
    https://www.ipa.go.jp/archive/publish/qv6pgp00000011mh-att/000064005.pdf

0. 前提

プログラムは一度書いた瞬間に保守対象になる。研究・開発の現場では、コードは装置・試料・データと同じく再現性の担保物であり、検証可能性が価値になる。 良さの尺度は、実行結果の正しさだけでなく、読みやすさ、変更のしやすさ、性能、セキュリティ、運用上の追跡可能性まで含む。

1. 良さを定義する

良さを言語化できないと、レビューや改善が感想戦になる。まず、良さを品質特性として固定する。

ソフトウェア品質を「外から見える品質」と「内側の品質」に分けると整理しやすい。外から見える品質は、機能の正しさ、性能、信頼性、安全性などである。内側の品質は、可読性、変更容易性、テスト容易性、局所性(変更の影響範囲が狭いこと)などである。内側の品質は外側の品質の土台である。

ISO/IEC 25010 の品質特性は、議論を共通言語化するのに役立つ。特に、保守性(maintainability)を中心に据えると、命名、分割、依存、テスト、ドキュメント、解析ツールが一本の線でつながる。

2. 読み手を想定した可読性

可読性は、好みではなく情報伝達の設計である。可読性が高いコードは、読み手が頭の中で保持する情報量が小さい。

2.1 命名

命名は最も費用対効果が高い改善である。良い名前は、読み手の推論を減らし、仕様の探索コストを下げる。

  • 役割が分かる名詞と動詞を選ぶ(データは名詞、処理は動詞)
  • 単位・座標系・基準(例:eV, nm, strain, normalized)を名前に埋め込む
  • 省略はチーム内の辞書がある場合に限定する
  • 同じ概念に複数の名前を使わない(同義語の乱立を避ける)
  • 否定語を避ける(not, non, disable などは二重否定を生みやすい)

研究コードでは、物理量名に下付きや上付きが付くことが多い。コード上では、表記ゆれを抑えた写像(例:epsilon_xx, sigma_xy, m_orb, m_spin)を決め、ノートや論文と対応づける。

2.2 構造

読み手は上から下へ読む。したがって、上位の意図から下位の詳細へ降りる構造を作る。

  • ファイル:目的単位で分ける(機能別、層別、物理モデル別)
  • 関数:一つの意図に絞る(複数の理由で変更される関数は崩れやすい)
  • クラス:状態と不変条件(invariant)を中心に設計する
  • 依存:入口を細くし、出口を揃える(入力形式と出力形式の揺れを抑える)

関数やクラスの先頭には、その単位で守るべき不変条件を書くのが効く。不変条件は、単なる説明文ではなく、後述のテストとアサーションの設計図である。

2.3 形式の統一

形式は、人間の注意を消耗させる。形式を自動化し、議論を止める。

  • フォーマッタを入れる(差分が意味に集中する)
  • import の順序や空白、改行、行長を揃える
  • 記法の揺れ(例:snake_case と camelCase の混在)を避ける

形式統一は、レビュアーの時間を意味の検査に振り向けるための投資である。

3. 変更容易性の設計

変更容易性は、未来の要件変更を前提にした力学である。ソフトウェアは変更されることを前提にした人工物である。

3.1 分割と境界

良い分割は、変更が境界を越えにくい。境界は、物理モデル、データ形式、外部I/O、可視化、最適化などで切りやすい。

  • 物理モデル:式とパラメータ、境界条件、初期条件
  • 数値計算:離散化、ソルバ、収束判定、安定化
  • データ:読み込み、正規化、欠損処理、バージョニング
  • 可視化:描画仕様、図の再現性、出力の命名規則

研究コードで頻発する崩れは、解析ロジックとファイルI/Oと描画が絡み合うことである。責務を分けるだけで再利用性が急に上がる。

3.2 依存の方向

依存は上流から下流へ流す。抽象に依存し、具体に依存しない設計は、実験条件の差し替えや計測器の変更に強い。

  • 外部サービスや装置は、薄いアダプタ層で隔離する
  • 数値ソルバは、インタフェースを固定し実装を差し替えられるようにする
  • データ形式は、スキーマを明示し、変換点を一箇所に集める

3.3 状態と副作用の制御

副作用は追跡が難しい。状態の持ち方を制御すると、バグの探索範囲が縮む。

  • グローバル変数を減らす
  • 関数を可能な限り純粋にする(同じ入力なら同じ出力)
  • 乱数や時刻などの非決定性は、注入して固定できるようにする

4. 正しさを支えるテスト

良いコードは、正しさを主張するだけでなく、正しさの検証方法を内蔵する。テストは機能追加のためではなく、変更を安全にするための装置である。

4.1 テストの層

テストは速度と信頼性のトレードオフで層に分かれる。

対象速度失敗時の原因特定主な役割
ユニット小さな関数・クラス速い容易ロジックの局所正しさ
結合複数部品の接続中間中間境界条件・整合性
E2E外部I/Oを含む遅い困難全体の回帰検証

ユニットを厚くし、上層を薄くする考え方がよく用いられる。E2E は価値が高いが高コストであり、増やしすぎると全体速度が落ちるため、層のバランスが重要である。

4.2 数値計算・研究コードのテスト

研究コードでは、正解データが無いことが多い。その場合は不変条件と整合性で固める。

  • 次元解析(単位が揃うか)
  • 保存則(エネルギー、質量、磁束など)
  • 対称性(回転、反転、並進、交換対称など)
  • 収束性(メッシュや刻み幅を細かくしたときの極限)
  • 退化ケース(ゼロ場、等方、既知解がある極限)

不変条件は、アサーションにも落とせる。例えば、確率分布なら総和が 1 である、正定値行列なら固有値が非負である、などである。

5. 静的解析と型

テストが実行に基づく検査だとすると、静的解析は構造に基づく検査である。静的解析は、人が見落とす類の誤りを早期に減らす。

5.1 リンタとフォーマッタ

リンタはバグの芽と規約違反を検出する。フォーマッタは表記を機械的に統一する。

  • Python:Ruff や Pylint などを用途で使い分ける
  • C/C++:clang-tidy などの静的解析器を活用する

重要なのは、検出結果を人手で放置しないことである。警告をゼロに保つ運用は、後戻りコストを下げる。

5.2 型ヒントと型検査

型は、関数の契約を機械に伝える道具である。動的言語でも型ヒントを足すと、実行前に誤りを見つけやすい。

  • 入力と出力の型を宣言する
  • 形状(配列の次元)や単位をドキュメントと命名で補う
  • 例外が投げられる条件を明記する

型検査は、設計の粗を浮かび上がらせる。たとえば、None が混ざる、Optional の扱いが曖昧、外部I/Oの戻り値が不安定、といった点が早期に露出する。

6. エラー処理と例外設計

エラー処理は、プログラムの信頼性を決める。異常系を設計しないと、正常系も保てない。

6.1 エラーの分類

エラーを分類して扱いを決める。

  • 事前条件違反(入力が不正)
  • 外部要因(ネットワーク、ファイル、装置)
  • 計算の破綻(発散、非収束、数値不安定)
  • 実装バグ(到達してはいけない分岐)

分類をすると、例外にすべきか、戻り値にすべきか、リトライすべきかが決まる。

6.2 例外の設計

例外は、復旧可能性と責務で設計する。

  • 例外型を階層化し、呼び出し側が捕捉できる粒度を用意する
  • 例外メッセージに、入力条件と識別子(ファイル名、試料ID、計測条件)を含める
  • 例外の握りつぶしを避け、必要なら明示的に理由を残す

C++ の場合、資源管理と例外は RAII の考え方と相性が良い。資源(メモリ、ロック、ファイル)をオブジェクト寿命に結びつけると、例外経路でも解放漏れが減る。

7. ログと追跡可能性

再現性の観点では、ログは結果の一部である。ログはデバッグ出力ではなく、実験ノートの機械版である。

  • 重要な入力(パラメータ、乱数シード、環境情報)を残す
  • ステップ境界(前処理、学習、推論、可視化)で区切る
  • エラー時に、再現に必要な最小情報を出す

研究コードでは、計算条件がわずかに違うだけで結論が変わることがある。したがって、条件を機械的に記録する習慣が重要である。

8. 性能と計測

性能最適化は、推測から始めない。計測に基づく。

  • まず複雑度の見積もりをする(オーダの把握)
  • 次にボトルネックを計測で特定する
  • 最後に局所最適化を施し、再度計測する

計算量は概念的に

T(n)=O(f(n))

で表されるが、現実の支配要因はメモリアクセスやI/Oであることも多い。数値計算では、配列形状とデータ局所性が支配要因になりやすい。

9. セキュリティの基本

研究コードでも、外部入力(ファイル、ネットワーク、ユーザ入力)がある限りセキュリティ問題は起こり得る。安全性は後付けではなく、設計に織り込む。

  • 入力検証(形式、範囲、サイズ)
  • 認証情報の取り扱い(鍵・トークンをコードに埋めない)
  • 依存ライブラリの更新と脆弱性情報の追跡
  • 権限分離(ファイル書き込み先、実行権限)

外部公開する解析コードや学会配布ツールでは、入力が敵対的である可能性も想定するのが堅い。

10. ドキュメントとコメント

コメントは不要だと言われることがあるが、不要なのは「コードを見れば分かることの繰り返し」である。必要なのは意図、前提、根拠である。

10.1 何を書くか

  • なぜその実装なのか
  • どの仮定の下で成立するのか
  • 数式や文献との対応
  • 入力の単位と座標系
  • 近似と誤差の見積もり

10.2 docstring

docstring は、インタフェース契約の記述場所として強力である。引数、戻り値、副作用、例外条件を一貫して残すと、保守性が上がる。

11. コードレビュー

レビューは、欠陥検出だけでなく、設計知識の共有と一貫性の維持である。レビューが機能するためには、目的と合格基準が必要である。

  • 変更単位を小さくする(差分の意味密度を上げる)
  • 仕様の根拠をレビュー対象に含める(チケット、論文、測定条件)
  • レビュアーは、将来の保守者として読む
  • 指摘は人格ではなく成果物に向ける
  • 合意した規約は機械で強制し、レビューでは意味に集中する

12. リファクタリングと技術的負債

短期の都合で内部品質を落とすと、将来の変更で利子が増える。この状態を技術的負債と呼ぶ。

技術的負債を数式で比喩化すると、変更コストは

Cchange(t)=C0+0tI(τ)dτ

のように表せる。I(τ) は利子に相当し、複雑化、重複、欠陥密度、知識の欠落が増えるほど大きくなる。

負債の管理は、ゼロにすることではなく、返済計画を持つことである。

  • 返済候補を記録する(小さな改善でも残す)
  • 返済の基準を決める(頻繁に触る部分から)
  • 返済を小さく刻む(大改修は失敗しやすい)
  • 返済が正しいことをテストで保証する

13. 研究コードの再現性

研究コードでは、正しさは再現性で検証される。計算結果が再現できないと、議論が止まる。

  • 実行環境を固定する(OS、コンパイラ、ライブラリ、GPUドライバ)
  • データとコードの対応を残す(入力データのハッシュ、生成条件)
  • 乱数と非決定性を制御する(seed、並列化の影響)
  • 結果の生成手順を一つにまとめる(手順の分岐を減らす)

再現性は、将来の論文の追試と、研究室内の引き継ぎの両方に効く。

14. チームで揃える規約

個人の工夫だけでは、共同研究では破綻する。最低限の取り決めで、摩擦を減らす。

  • コーディング規約(命名、形式、ディレクトリ構造)
  • コミットメッセージ規約(変更意図が追える)
  • ブランチ戦略(保守と開発の分離)
  • リリース手順(タグ、変更履歴)

規約は、人が守るのではなく、機械が守らせる方向が望ましい。

15. 学習の順序

良いコードを書く力は、技術よりも習慣で伸びる。習慣化しやすい順に積む。

  1. 形式統一(フォーマッタ)
  2. 命名と分割(短い関数、薄い責務)
  3. テスト(不変条件と回帰)
  4. 静的解析(リントと型)
  5. 依存と境界(設計の再利用)
  6. 性能(計測に基づく改善)
  7. セキュリティ(入力と秘密情報の管理)

まとめと展望

良いコードは、可読性、変更容易性、検証可能性、再現性が同時に満たされる方向へ設計された成果物であり、研究でも開発でも生産性を支配する基盤である。今後は、静的解析・型・テストの自動化がさらに進み、コードは個人技能の表現から、検証可能性を組み込んだ研究インフラへと比重が移っていくと考えられる。

その他参考文献