CS/SE Fundamentals

ソフトウェアエンジニアリングの実践 (Software Engineering Practices)

はじめに

優れたソフトウェアは、優れたコードだけでは作れない。バージョン管理、テスト、CI/CD、コードレビュー -- これらのプロセスと実践が、チームとして持続的に価値を届ける力を支えている。

この章では、日常の開発業務で実践すべき SE の基本を解説する。


バージョン管理 (Git)

Git の基本モデル

Git はスナップショットベースの分散バージョン管理システム。

Working Directory → Staging Area → Local Repository → Remote Repository
     (作業)          (git add)       (git commit)        (git push)

重要な概念:

  • コミット: スナップショット + メタデータ (著者、日時、メッセージ)
  • ブランチ: コミットの系列への軽量なポインタ
  • マージ: 異なるブランチの変更を統合
  • リベース: コミット履歴を直線的に整理

ブランチ戦略

GitHub Flow (シンプルで推奨):

main ─────────────────────────────────────────→
       \                    /
        feature/add-patient-search ──→ PR → merge
  1. main から feature ブランチを作成
  2. 変更をコミット
  3. Pull Request を作成
  4. レビューを受けてマージ
  5. feature ブランチを削除

Git Flow (リリース管理が複雑な場合):

main    ─────────────────────────────────→
            \         /
develop ─────────────────────────────────→
              \     /
release/1.0 ───────→
               \
feature/xxx ────→

実務での選択: 大半のプロジェクトでは GitHub Flow で十分。リリースサイクルが複雑な場合のみ Git Flow を検討。

コミットメッセージ

良いコミットメッセージは「なぜ」を伝える。

# Bad
fix bug
update code
WIP

# Good
fix: 予約時間の重複チェックが午前0時をまたぐ場合に失敗する問題を修正

患者が23:00-01:00のような日をまたぐ時間帯を指定した場合、
重複チェックのロジックが正しく動作していなかった。
start_time と end_time の比較を日付を考慮した形に修正。

Refs: RAICA-1234

Conventional Commits の形式が広く使われている:

<type>(<scope>): <description>

feat: 新機能
fix: バグ修正
docs: ドキュメントのみの変更
refactor: リファクタリング
test: テストの追加・修正
chore: ビルドプロセスやツールの変更

コードレビュー

レビューの目的

  1. バグの早期発見: 本番に出る前に問題を見つける
  2. 知識の共有: チームメンバーが他の部分のコードを理解する
  3. 設計の改善: 複数の視点でより良い設計を探る
  4. 品質基準の維持: コーディング規約やベストプラクティスの徹底

効果的なレビューの仕方

レビュアーとして:

  • まず PR の目的と背景を理解する
  • 設計レベルの問題を優先する (命名の好みよりアーキテクチャの問題)
  • 「なぜ」を問う質問をする (「これはなぜこの方法を選んだ?」)
  • 具体的な提案を含める (「こうすべき」ではなく「こうするのはどうか」)
  • コードの良い点も指摘する

レビューイとして:

  • PR のサイズを小さく保つ (200-400行が目安)
  • PR の説明を丁寧に書く (変更の目的、影響範囲、テスト方法)
  • セルフレビューを先にする
  • フィードバックを個人攻撃と受け取らない

レビューチェックリスト

□ 機能要件を満たしているか
□ エッジケースは考慮されているか
□ セキュリティ上の問題はないか (入力バリデーション、認可チェック)
□ パフォーマンス上の懸念はないか (N+1、不要な計算)
□ テストは十分か
□ エラーハンドリングは適切か
□ 命名は明確か
□ 既存のパターンや規約に従っているか

テスト戦略

テストピラミッド

        /  E2E テスト  \          少数・高コスト・遅い
       /  (10-20%)     \
      /─────────────────\
     / Integration テスト \       適度な数
    /   (20-30%)          \
   /───────────────────────\
  /     Unit テスト          \    大量・低コスト・高速
 /      (50-70%)              \
/──────────────────────────────\

Unit テスト

個々の関数やクラスを単独でテストする。

// テスト対象
function calculateAge(birthDate: Date, referenceDate: Date): number {
  let age = referenceDate.getFullYear() - birthDate.getFullYear();
  const monthDiff = referenceDate.getMonth() - birthDate.getMonth();
  if (monthDiff < 0 || (monthDiff === 0 && referenceDate.getDate() < birthDate.getDate())) {
    age--;
  }
  return age;
}

// テスト
describe('calculateAge', () => {
  it('誕生日前は1歳引かれる', () => {
    const birth = new Date('1990-06-15');
    const ref = new Date('2026-03-06');
    expect(calculateAge(birth, ref)).toBe(35);
  });

  it('誕生日当日は加算される', () => {
    const birth = new Date('1990-03-06');
    const ref = new Date('2026-03-06');
    expect(calculateAge(birth, ref)).toBe(36);
  });
});

Unit テストの原則:

  • 速い: 数ミリ秒で完了
  • 独立: 他のテストに依存しない
  • 再現性: 何度実行しても同じ結果
  • 外部依存をモック: DB、API、ファイルシステムはモックする

Integration テスト

複数のコンポーネントが正しく連携するかをテストする。

// API エンドポイントのテスト
describe('POST /api/appointments', () => {
  it('有効な予約を作成できる', async () => {
    const response = await request(app)
      .post('/api/appointments')
      .set('Authorization', `Bearer ${validToken}`)
      .send({
        patientId: 'patient-123',
        doctorId: 'doctor-456',
        startTime: '2026-03-10T10:00:00Z',
      });

    expect(response.status).toBe(201);
    expect(response.body.status).toBe('scheduled');
  });

  it('時間が重複する場合は409を返す', async () => {
    // 既存の予約を作成
    await createAppointment({ ... });

    const response = await request(app)
      .post('/api/appointments')
      .set('Authorization', `Bearer ${validToken}`)
      .send({ /* 同じ時間帯 */ });

    expect(response.status).toBe(409);
  });
});

E2E テスト (End-to-End)

ユーザーの実際の操作フローをテストする。Playwright, Cypress などを使用。

// E2E テスト (Playwright)
test('患者を検索して予約を作成できる', async ({ page }) => {
  await page.goto('/appointments/new');
  await page.fill('[data-testid="patient-search"]', '田中');
  await page.click('[data-testid="patient-result-0"]');
  await page.click('[data-testid="timeslot-10-00"]');
  await page.click('[data-testid="confirm-button"]');

  await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
});

E2E テストの注意点:

  • 実行が遅い → 重要なフローに絞る
  • フレイキー (不安定) になりやすい → 適切な待機処理を入れる
  • メンテナンスコストが高い → 安定したセレクターを使う

CI/CD

CI (Continuous Integration)

コードの変更を頻繁にメインブランチに統合し、自動テストで品質を確認する。

# GitHub Actions の例
name: CI
on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm test

CI で実行すべきこと:

  • リント (ESLint, Prettier)
  • 型チェック (TypeScript)
  • Unit テスト + Integration テスト
  • セキュリティスキャン (npm audit)
  • ビルドの検証

CD (Continuous Delivery / Deployment)

  • Continuous Delivery: いつでもデプロイできる状態を維持
  • Continuous Deployment: テストが通れば自動的にデプロイ
コードプッシュ → CI (テスト・ビルド) → ステージング → (承認) → 本番

デプロイ戦略:

戦略説明リスク
ローリングデプロイ段階的にインスタンスを入れ替え
Blue/Green新旧環境を切り替え低 (ロールバックが容易)
カナリアリリース少数のユーザーに先行公開低 (問題を早期検出)
ビッグバンデプロイ一斉に切り替え

技術的負債の管理

技術的負債とは

「今は動くが、将来の変更を難しくする設計上の妥協」のこと。借金と同じで、放置すると利子 (追加の開発コスト) が膨らむ。

技術的負債の種類

種類対処
意図的・慎重"締切のためにテストを省略。次スプリントで追加"計画的に返済
意図的・無謀"テスト書く時間ないから書かない"避けるべき
無意識・慎重"より良い設計を後で発見した"学びとして受け入れ、改善
無意識・無謀"レイヤードアーキテクチャって何?"学習で防ぐ

管理の実践

  • 可視化する: 技術的負債をチケットとして記録
  • 定期的に返済する: スプリントの一定割合 (例: 20%) を負債返済に充てる
  • 新規の負債を抑制する: コードレビューで品質を担保
  • 影響度で優先順位をつける: 変更が頻繁な箇所の負債を優先的に返済

ドキュメンテーション

書くべきドキュメント

種類対象読者
ADR (Architecture Decision Record)チームアーキテクチャ上の意思決定の記録
API ドキュメントAPI 利用者OpenAPI/Swagger
Runbook運用担当障害対応の手順書
オンボーディングガイド新メンバー開発環境構築、コード概要

ADR の形式

# ADR-001: メッセージキューに AWS SQS を採用

## ステータス
承認済み (2026-03-01)

## コンテキスト
非同期処理のためのメッセージキューが必要。

## 決定
AWS SQS を採用する。

## 理由
- AWS エコシステムとの統合が容易
- マネージドサービスで運用負荷が低い
- チームに AWS の知見がある

## 却下した選択肢
- RabbitMQ: 自前でホスティングが必要
- Kafka: 現在の規模にはオーバースペック

## 影響
- SQS のメッセージサイズ上限 (256KB) を考慮した設計が必要

Agent-first 開発においてこれが重要な理由

SE の実践は、AI コーディングエージェントとの協業の品質を直接左右する。

  • テストを指示できる: 「この関数の Unit テストを書いて。エッジケースとして空配列と null も含めて」と具体的にテスト戦略を伝えられる
  • CI の失敗を診断できる: エージェントに「lint エラーを修正して」「型エラーを解消して」と的確な修正指示ができる
  • コミットの粒度を制御できる: 「この変更はリファクタリングと機能追加で別のコミットにして」と分割を指示できる
  • 技術的負債を認識できる: エージェントの出力が負債を増やしていないかを評価し、必要に応じて改善を指示できる
  • PR の品質を担保できる: エージェントが作成した PR の説明文やテストカバレッジが適切かを判断できる

エージェントは「コードを書く」が、「ソフトウェアを届ける」にはプロセス全体の理解が必要だ。


Further Reading