言語やフレームワークに依存しない、ソフトウェア実装の基本原則をまとめたガイドです。
目次
1. 着手前の準備
- 要件を言語化する: 「何を」「なぜ」作るのかを1〜2文で説明できる状態にしてから書き始める。
- 完了条件を決める: 受け入れ基準(入力・出力・エッジケース)を先に書き出す。
- 既存コードを読む: 同じ問題がすでに解決されていないか、プロジェクトの慣習(命名・構成・エラー処理)を確認する。
- 小さく分割する: 1つの変更は1つの目的に絞る。大きな機能は段階的にマージできる単位に分ける。
2. 設計の原則
| 原則 | 要点 |
|---|---|
| KISS (Keep It Simple) | 最も単純に動く方法を選ぶ。賢さより読みやすさ。 |
| YAGNI | 今必要ない機能・抽象化は作らない。 |
| DRY | 知識(ルール)の重複を避ける。ただし見た目が似ているだけのコードを無理に共通化しない(3回目で共通化を検討)。 |
| 単一責任 | 1つの関数・クラスは1つの理由でのみ変更されるようにする。 |
| 疎結合・高凝集 | 依存は最小限に、関連するものは近くに置く。 |
| 依存の方向を意識する | ビジネスロジックがフレームワーク・DB・外部APIに直接依存しないよう境界を設ける。 |
3. コードの書き方
命名
- 意図が伝わる名前にする(
data,tmp,flagを避ける)。 - 真偽値は
is/has/canなどで始める。 - 関数名は動詞、クラス名・変数名は名詞。
- 略語はチーム内で一般的なものだけ使う。
関数・構造
- 関数は短く、1つのことだけを行う。
- 引数は少なく(目安3つ以内)。多い場合はオブジェクトにまとめる。
- 早期リターンでネストを浅くする。
- マジックナンバーは名前付き定数にする。
- 副作用のある処理と純粋な計算を分離する。
# NG: ネストが深い
def process(user):
if user:
if user.is_active:
if user.has_permission:
return do_work(user)
return None
# OK: 早期リターン
def process(user):
if not user or not user.is_active:
return None
if not user.has_permission:
return None
return do_work(user)コメント
- 「何を」ではなく「なぜ」 を書く。コードを読めば分かることは書かない。
- 一時的な回避策には理由とチケット番号を残す(
# TODO(#123): ...)。 - コメントアウトしたコードは残さず削除する(履歴はGitにある)。
4. エラー処理
- 早く失敗する (Fail Fast): 不正な入力は境界(API・関数の入口)で検証し、すぐにエラーにする。
- 例外を握りつぶさない:
catchして何もしない・ログだけ出して続行する、は原則禁止。 - 適切な粒度で捕捉する: 汎用例外ではなく、想定する具体的な例外を捕捉する。
- エラーメッセージに文脈を含める: 何をしようとして、何が、なぜ失敗したか。
- ユーザー向けと開発者向けを分ける: 内部情報(スタックトレース・SQL)をユーザーに見せない。
- リソースは確実に解放する:
try-with-resources/with/defer/finallyを使う。
5. テスト
- テストしやすい設計にする: 依存を注入可能にし、副作用を境界に寄せる。
- テストピラミッド: ユニットテストを多く、統合テストを適度に、E2Eテストは重要な導線に絞る。
- 1テスト1観点: テスト名で「何を検証しているか」が分かるようにする。
- Arrange / Act / Assert の3段構成で書く。
- 境界値・異常系を必ず含める: 空、null、0、最大値、重複、不正形式など。
- バグ修正時は再現テストを先に書く: 再発防止になる。
- テストは決定的に(時刻・乱数・外部通信は固定またはモック化)。
6. セキュリティ
- 外部入力はすべて信用しない: バリデーションとサニタイズを行う。
- SQLはパラメータ化クエリを使う(文字列結合で組み立てない)。
- 出力時にエスケープしてXSSを防ぐ。
- 秘密情報をコードに書かない: APIキー・パスワードは環境変数やシークレット管理サービスへ。
.envはリポジトリに含めない。 - 最小権限の原則: アカウント・トークン・DBユーザーには必要最低限の権限のみ付与する。
- 依存ライブラリを定期的に更新し、脆弱性スキャンを行う。
- 認証・暗号化は自作せず、実績のあるライブラリを使う。
7. パフォーマンス
- まず正しく、次に速く: 推測で最適化しない。計測してボトルネックを特定してから手を入れる。
- N+1クエリに注意する(ループ内でのDB・API呼び出し)。
- 計算量(O記法)を意識し、大量データでの挙動を想定する。
- キャッシュは無効化戦略とセットで導入する。
- 重い処理は非同期化・バッチ化を検討する。
8. ログと監視
- ログレベルを使い分ける:
ERROR(要対応)/WARN(注意)/INFO(主要イベント)/DEBUG(開発時の詳細)。 - 構造化ログ(JSONなど)で検索・集計しやすくする。
- リクエストIDなどの相関IDを付けて追跡可能にする。
- 個人情報・秘密情報をログに出さない。
- 重要な指標(エラー率・レイテンシ・処理件数)を監視し、アラートを設定する。
9. バージョン管理(Git)
- コミットは小さく、意味のある単位で。1コミット1目的。
- コミットメッセージは「何を・なぜ」を書く(例:
fix: ログイン時のセッション切れでリダイレクトされない問題を修正)。 - ブランチは短命に保ち、こまめにメインブランチを取り込む。
- 生成物・依存パッケージ・秘密情報は
.gitignoreで除外する。 - フォーマット・リント修正と機能変更は別コミットにする。
10. コードレビュー
レビューを依頼する側
- PRは小さく(目安: 差分400行以内)。
- 説明欄に「目的・変更内容・確認方法・影響範囲」を書く。
- セルフレビューしてから依頼する。
レビューする側
- 観点: 正しさ → 設計 → 可読性 → テスト → 細かなスタイル の順に重視する。
- 指摘には理由を添え、必須(must)か提案(nit / imo)かを明示する。
- 人ではなくコードについて話す。
11. 自動化とツール
- フォーマッタ・リンターを導入し、スタイル議論を機械に任せる(例: Prettier, ESLint, Black, Ruff, gofmt)。
- CIでテスト・リント・型チェック・ビルドを自動実行し、通らないものはマージしない。
- 型システム(TypeScript、型ヒント等)を活用してバグを早期に検出する。
- 繰り返す手作業はスクリプト化する。
12. ドキュメント
- README: 概要、セットアップ手順、実行・テスト方法を最低限記載する。
- 設計上の重要な判断は ADR(Architecture Decision Record) として残す。
- 公開API・関数には使い方と前提条件を書く。
- ドキュメントはコードと同じリポジトリで管理し、変更時に一緒に更新する。
13. リファクタリング
- ボーイスカウトルール: 触ったコードは来たときより少しきれいにして去る。
- リファクタリングと機能追加を同時に行わない。
- テストで振る舞いを保証してから構造を変える。
- 大規模な変更は段階的に、常に動く状態を保ちながら進める。
チェックリスト(PR前の最終確認)
- 要件・受け入れ基準を満たしている
- 正常系・異常系・境界値のテストがある
- CI(テスト・リント・型チェック)が通る
- エラーが適切に処理され、握りつぶしていない
- 秘密情報・個人情報がコードやログに含まれていない
- 外部入力のバリデーションがある
- 命名が分かりやすく、不要なコード・コメントが残っていない
- 必要なドキュメント(README・コメント・ADR)を更新した
- 差分が1つの目的に絞られている