第5章: ソフトウェア設計原則と実装方法論 - 理論に基づく堅牢な実装

5.1 ソフトウェア設計原則の歴史と理論

5.1.1 モジュール化の起源

ソフトウェアのモジュール化は、1960年代後半のソフトウェア危機(Software Crisis)への対応として発展しました。1968年のNATO Software Engineering Conferenceで、大規模ソフトウェア開発の困難さが正式に認識されました。

ソフトウェア危機(1968年):

問題:
- 予算超過: プロジェクトの90%が予算を超過
- 遅延: 納期遅れが常態化
- 品質問題: バグの多発、保守困難

原因:
- プログラムの複雑化(スパゲッティコード)
- 変更の影響範囲が予測不能
- コードの再利用が困難

解決策:
- 構造化プログラミング(Dijkstra, 1968)
- モジュール設計(Parnas, 1972)
- 抽象データ型(Liskov, 1974)

5.1.2 情報隠蔽の原則(Parnas, 1972)

David Parnasは1972年の論文「On the Criteria To Be Used in Decomposing Systems into Modules」で、モジュール分解の基準を提唱しました。

従来のアプローチ(フローチャート分解)

┌──────────────────────────────────────────┐
│ メインプログラム                          │
│                                          │
│ 1. ファイルを開く                         │
│ 2. バイトを読む                           │
│ 3. 改行をチェック                         │
│ 4. 文字列を結合                           │
│ 5. 行を返す                              │
│ 6. ファイルを閉じる                       │
│                                          │
│ 問題: すべてのステップが密結合            │
└──────────────────────────────────────────┘

Parnasのアプローチ(情報隠蔽)

┌────────────────────┐
│  get_next_line()   │ ← 公開インターフェース
│  (抽象層)          │
└─────────┬──────────┘
          │
    ┌─────┴─────┐
    ▼           ▼
┌────────┐  ┌────────┐
│ Buffer │  │ String │ ← 実装詳細(隠蔽)
│ Module │  │ Module │
└────────┘  └────────┘

各モジュールは「設計決定」を隠蔽:
- Buffer Module: バッファサイズ、読み取り戦略
- String Module: 文字列表現、メモリ管理方法

Parnasの基準:

/*
 * モジュール分解の基準(Parnas, 1972):
 *
 * 1. 各モジュールは「変更されやすい設計決定」を隠す
 * 2. インターフェースは「変更されにくい」ものに依存
 * 3. モジュール間の依存は最小限に
 *
 * get_next_lineへの適用:
 *
 * 隠蔽すべき設計決定:
 *   - BUFFER_SIZE(変更される可能性大)
 *   - 静的変数の存在(実装詳細)
 *   - 文字列結合の方法(最適化で変更される可能性)
 *
 * 公開インターフェース:
 *   - char *get_next_line(int fd)
 *   - 入力: ファイルディスクリプタ
 *   - 出力: 1行の文字列(または NULL)
 */

5.1.3 結合度と凝集度(Constantine & Yourdon, 1979)

Larry ConstantineとEdward Yourdonは、モジュール設計の品質を測定する2つの指標を提唱しました。

結合度(Coupling): モジュール間の依存の強さ

高結合(悪い):
┌─────────┐    共有変数    ┌─────────┐
│ Module A │────────────→│ Module B │
│          │←────────────│          │
└─────────┘    相互依存    └─────────┘

低結合(良い):
┌─────────┐    引数渡し    ┌─────────┐
│ Module A │─────────────→│ Module B │
│          │←────────────│          │
└─────────┘    戻り値のみ   └─────────┘

結合度の種類(弱い順):

1. データ結合(最良): 単純なデータのみを引数で渡す
   例: char *ft_strlen(const char *s)

2. スタンプ結合: 構造体を渡すが、一部のみ使用
   例: void print_name(struct person *p)  // 名前のみ使用

3. 制御結合: 制御フラグを渡す
   例: void process(int fd, int mode)  // modeで動作が変化

4. 外部結合: 外部データ構造を共有
   例: グローバル変数を参照

5. 共通結合: グローバル領域を変更
   例: グローバル変数を変更

6. 内容結合(最悪): 他のモジュールの内部を直接変更
   例: 他の関数のローカル変数を変更

凝集度(Cohesion): モジュール内の要素の関連性

高凝集(良い):
┌─────────────────────────┐
│ String Module           │
│ ├─ ft_strlen()         │
│ ├─ ft_strchr()         │  すべて文字列操作に関連
│ └─ ft_strjoin()        │
└─────────────────────────┘

低凝集(悪い):
┌─────────────────────────┐
│ Utility Module          │
│ ├─ ft_strlen()         │
│ ├─ get_current_time()  │  関連性なし
│ └─ calculate_tax()     │
└─────────────────────────┘

凝集度の種類(強い順):

1. 機能的凝集(最良): 単一の明確な機能
   例: ft_strlen() - 文字列の長さを返す

2. 順序的凝集: 出力が次の入力に
   例: read() → parse() → store()

3. 通信的凝集: 同じデータを操作
   例: 同じファイルに対する読み書き

4. 時間的凝集: 同じタイミングで実行
   例: 初期化関数(init_all)

5. 論理的凝集: 論理的に似た操作
   例: print_int(), print_string()をまとめる

6. 偶発的凝集(最悪): 関係ない要素の寄せ集め

5.1.4 SOLID原則の起源

Robert C. Martin(Uncle Bob)は2000年代初頭にSOLID原則を体系化しました。C言語にも適用可能です。

S - Single Responsibility Principle(単一責任の原則)
    1つのモジュールは1つの理由でのみ変更される

    良い例:
    char *extract_line(char *remainder);  // 行抽出のみ
    char *update_remainder(char *old);    // 残り更新のみ

    悪い例:
    char *do_everything(int fd);  // 読み取り + 抽出 + 更新

O - Open/Closed Principle(開放閉鎖原則)
    拡張に開かれ、修正に閉じている

    例: BUFFER_SIZEをマクロで定義
    #ifndef BUFFER_SIZE
    # define BUFFER_SIZE 4096
    #endif
    → コンパイル時に変更可能(ソース修正不要)

L - Liskov Substitution Principle(リスコフの置換原則)
    サブタイプは基底型と置換可能

    C言語では: 関数ポインタの互換性
    typedef char *(*line_reader_t)(int fd);
    line_reader_t reader = get_next_line;  // 互換性あり

I - Interface Segregation Principle(インターフェース分離原則)
    クライアントに不要なインターフェースへの依存を強制しない

    get_next_line.h は必要最小限:
    char *get_next_line(int fd);  // これだけ公開

D - Dependency Inversion Principle(依存性逆転の原則)
    高レベルモジュールは低レベルモジュールに依存しない

    get_next_line() → ft_strjoin()
    抽象(インターフェース)への依存、実装への非依存

5.2 契約による設計(Design by Contract)

5.2.1 契約プログラミングの理論

Bertrand Meyerは1986年にEiffel言語で「契約による設計」を提唱しました。これは各関数を「契約」として定義する手法です。

契約の3要素:

1. 事前条件(Preconditions)
   関数呼び出し前に満たすべき条件
   呼び出し側の責任

2. 事後条件(Postconditions)
   関数終了時に保証される条件
   関数の責任

3. 不変条件(Invariants)
   操作前後で維持される条件
   データ構造の整合性

5.2.2 Hoare論理による形式検証

Tony Hoareは1969年に「Hoareの三つ組」を提唱しました。これはプログラムの正しさを数学的に証明する手法です。

Hoareの三つ組:

{P} S {Q}

ここで:
  P = 事前条件
  S = プログラム文
  Q = 事後条件

意味: Pが真のとき、Sを実行すると、Qが真になる

get_next_lineへの適用:

/*
 * get_next_line の契約:
 *
 * {P} fd >= 0 ∧ BUFFER_SIZE > 0 ∧ fdは有効なオープン状態
 *
 * char *line = get_next_line(fd);
 *
 * {Q} (line ≠ NULL ⟹ lineは改行を含む文字列 ∨ lineはEOF前の残り)
 *     ∧ (line = NULL ⟹ EOF到達 ∨ エラー発生)
 *     ∧ 次回呼び出しは次の行を返す
 */

char *get_next_line(int fd)
{
    // 事前条件のチェック(防御的プログラミング)
    if (fd < 0 || BUFFER_SIZE <= 0)
        return (NULL);  // 契約違反時は安全に失敗

    // ... 実装 ...

    // 事後条件: lineは有効な文字列かNULL
    return (line);
}

5.2.3 関数ごとの契約定義

/**
 * ft_strlen - 文字列の長さを計算
 *
 * 契約:
 *   事前条件: s ≠ NULL ∧ sはヌル終端文字列
 *   事後条件: 戻り値 = sの文字数(ヌル終端を除く)
 *
 * @param s: 計測する文字列
 * @return: 文字列の長さ
 */
size_t ft_strlen(const char *s)
{
    size_t len;

    // assert(s != NULL);  // 事前条件の検証(デバッグ時)
    len = 0;
    while (s[len])
        len++;
    // assert(len >= 0);   // 事後条件の検証
    return (len);
}

/**
 * ft_strchr - 文字列中の文字を検索
 *
 * 契約:
 *   事前条件: s ≠ NULL ∧ sはヌル終端文字列
 *   事後条件:
 *     戻り値 ≠ NULL ⟹ *戻り値 = c ∧ 戻り値はs内のポインタ
 *     戻り値 = NULL ⟹ c ∉ s(ただしc='\0'の場合は終端を返す)
 */
char *ft_strchr(const char *s, int c)
{
    char ch;

    ch = (char)c;
    while (*s)
    {
        if (*s == ch)
            return ((char *)s);
        s++;
    }
    if (ch == '\0')
        return ((char *)s);
    return (NULL);
}

/**
 * ft_strjoin - 2つの文字列を結合
 *
 * 契約:
 *   事前条件: s1 ≠ NULL ∧ s2 ≠ NULL ∧ 両方ヌル終端
 *   事後条件:
 *     戻り値 ≠ NULL ⟹ 戻り値 = s1 || s2(連結)
 *                      ∧ 戻り値は新規確保メモリ
 *                      ∧ strlen(戻り値) = strlen(s1) + strlen(s2)
 *     戻り値 = NULL ⟹ malloc失敗
 *
 * メモリ責任: 呼び出し側が戻り値をfree()
 */
char *ft_strjoin(char const *s1, char const *s2)
{
    // 事前条件チェック
    if (!s1 || !s2)
        return (NULL);

    // ... 実装 ...
}

5.3 レイヤードアーキテクチャの理論

5.3.1 抽象化の階層

計算機科学では、複雑なシステムを抽象化の層に分割します。各層は下位層のサービスを使用し、上位層にサービスを提供します。

OSI参照モデル(通信):      get_next_lineの階層:

┌─────────────────┐        ┌─────────────────┐
│   Application   │        │  get_next_line  │ ← 公開API
├─────────────────┤        ├─────────────────┤
│  Presentation   │        │ read_and_store  │
├─────────────────┤        │ extract_line    │ ← 内部関数
│    Session      │        │ update_remainder│
├─────────────────┤        ├─────────────────┤
│   Transport     │        │   ft_strjoin    │
├─────────────────┤        │   ft_strchr     │ ← ユーティリティ
│    Network      │        │   ft_substr     │
├─────────────────┤        │   ft_strlen     │
│   Data Link     │        ├─────────────────┤
├─────────────────┤        │     read()      │ ← システムコール
│    Physical     │        │     malloc()    │
└─────────────────┘        └─────────────────┘

5.3.2 依存関係の管理

依存関係図:

get_next_line()
    │
    ├──→ read_and_store()
    │        ├──→ read()         [システムコール]
    │        ├──→ malloc()       [標準ライブラリ]
    │        ├──→ ft_strchr()    [ユーティリティ]
    │        ├──→ ft_strjoin()   [ユーティリティ]
    │        └──→ free()         [標準ライブラリ]
    │
    ├──→ extract_line()
    │        ├──→ ft_strchr()
    │        ├──→ ft_substr()
    │        └──→ ft_strlen()
    │
    └──→ update_remainder()
             ├──→ ft_strchr()
             ├──→ ft_substr()
             ├──→ ft_strlen()
             └──→ free()

依存の方向: 常に上から下へ(循環依存なし)

5.3.3 抽象化障壁(Abstraction Barrier)

SICP(Structure and Interpretation of Computer Programs)で提唱された概念です。

/*
 * 抽象化障壁:
 *
 * 上位層は下位層の実装詳細を知らない
 *
 * get_next_line()
 *   │
 *   │ 知っていること: ft_strjoin()は2つの文字列を結合する
 *   │ 知らないこと: ft_strjoin()がどのように実装されているか
 *   ▼
 * ═══════════════════════════════ 抽象化障壁 ═══════════════
 *   │
 *   │ ft_strjoin()の実装詳細:
 *   │   - mallocでメモリ確保
 *   │   - ループでコピー
 *   │   - ヌル終端
 *   ▼
 *
 * 利点:
 * 1. ft_strjoin()の実装を変更しても上位層に影響なし
 * 2. 上位層のロジックを理解しやすい
 * 3. テストを層ごとに独立して行える
 */

5.4 エラー処理の理論

5.4.1 エラー処理パラダイムの歴史

1. 戻り値によるエラー通知(1960s-)
   ret = function();
   if (ret == ERROR) { handle_error(); }

   利点: シンプル、オーバーヘッドなし
   欠点: 戻り値とエラーの混在、無視されやすい

2. グローバル変数errno(UNIX, 1970s)
   ret = read(fd, buf, size);
   if (ret < 0) { perror("read"); }

   利点: 戻り値を正常値に使える
   欠点: スレッドセーフでない(元々は)

3. 例外処理(C++, Java, 1990s)
   try { ... } catch (Exception e) { ... }

   利点: エラー処理を分離、無視できない
   欠点: 制御フローが複雑、オーバーヘッド

4. 代数的データ型(Rust, Haskell)
   Result<T, E> / Option<T>

   利点: 型安全、明示的
   欠点: 言語サポートが必要

5.4.2 C言語のエラー処理パターン

get_next_lineで使用するパターン:

/*
 * パターン1: NULLによるエラー通知
 *
 * 契約: 戻り値がNULLならエラーまたはEOF
 */
char *get_next_line(int fd)
{
    if (fd < 0)
        return (NULL);  // エラー

    // ...

    if (!line)
        return (NULL);  // EOFまたはエラー

    return (line);      // 成功
}

/*
 * パターン2: エラー伝播
 *
 * 下位関数のエラーを上位に伝える
 */
static char *read_and_store(int fd, char *remainder)
{
    ssize_t bytes_read;

    bytes_read = read(fd, buffer, BUFFER_SIZE);
    if (bytes_read < 0)
    {
        // エラー発生 → クリーンアップして伝播
        free(buffer);
        free(remainder);
        return (NULL);  // 上位にエラー通知
    }
    // ...
}

/*
 * パターン3: 早期リターン(Guard Clause)
 *
 * 異常ケースを先に処理し、正常フローを明確に
 */
char *get_next_line(int fd)
{
    // Guard clauses(異常ケース)
    if (fd < 0)
        return (NULL);
    if (BUFFER_SIZE <= 0)
        return (NULL);

    // 正常フロー(インデントが深くならない)
    remainder = read_and_store(fd, remainder);
    line = extract_line(remainder);
    remainder = update_remainder(remainder);

    return (line);
}

5.4.3 防御的プログラミング

/*
 * 防御的プログラミング(Defensive Programming):
 *
 * 「クライアントを信頼しない」
 * すべての入力を検証し、不正な状態から防御する
 */

// 防御的なft_strjoin
char *ft_strjoin_defensive(char const *s1, char const *s2)
{
    char    *joined;
    size_t  len1;
    size_t  len2;

    // 入力の検証(防御)
    if (!s1 && !s2)
        return (NULL);
    if (!s1)
        return (ft_strdup(s2));  // s1がNULLならs2のコピー
    if (!s2)
        return (ft_strdup(s1));  // s2がNULLならs1のコピー

    // オーバーフローの防御
    len1 = ft_strlen(s1);
    len2 = ft_strlen(s2);
    if (len1 > SIZE_MAX - len2 - 1)
        return (NULL);  // オーバーフロー防止

    // 正常処理
    joined = malloc(len1 + len2 + 1);
    if (!joined)
        return (NULL);

    // コピー処理...

    return (joined);
}

5.5 テスト理論と方法論

5.5.1 テストの分類

テストの種類:

1. 単体テスト(Unit Test)
   個々の関数を独立してテスト
   例: ft_strlen("hello") == 5

2. 統合テスト(Integration Test)
   複数のモジュールの連携をテスト
   例: read_and_store() + extract_line()

3. システムテスト(System Test)
   システム全体の動作をテスト
   例: get_next_line()の完全な動作

4. 回帰テスト(Regression Test)
   変更後に既存機能が壊れていないかテスト
   例: 最適化後に全テストを再実行

5.5.2 境界値分析(Boundary Value Analysis)

境界値分析:

バグは境界条件で発生しやすい

BUFFER_SIZEの境界:
  - BUFFER_SIZE = 0(無効)
  - BUFFER_SIZE = 1(最小有効値)
  - BUFFER_SIZE = 2(最小 + 1)
  - BUFFER_SIZE = MAX_INT(最大)

ファイル内容の境界:
  - 空ファイル
  - 1文字のみ(改行なし)
  - 改行のみ
  - 非常に長い1行
  - 最後に改行なし

fdの境界:
  - fd = -1(無効)
  - fd = 0(stdin)
  - fd = 1023(通常の上限付近)

5.5.3 同値分割(Equivalence Partitioning)

同値分割:

入力を同等の結果をもたらすグループに分割

ファイル内容のパーティション:
┌─────────────────┬──────────────────────────┐
│  パーティション  │        例                │
├─────────────────┼──────────────────────────┤
│  空ファイル      │  ""                      │
│  1行(改行あり) │  "hello\n"               │
│  1行(改行なし) │  "hello"                 │
│  複数行         │  "line1\nline2\n"        │
│  空行を含む     │  "line1\n\nline3\n"      │
│  長い行         │  "x" * 1000000 + "\n"    │
│  バイナリ       │  "\0\xff\x01\n"          │
└─────────────────┴──────────────────────────┘

各パーティションから1つテストケースを選ぶ

5.5.4 テスト駆動開発(TDD)

Kent Beckが1990年代に体系化したテスト駆動開発:

TDDサイクル:

1. Red: 失敗するテストを書く
2. Green: テストをパスする最小限のコードを書く
3. Refactor: コードを改善する

get_next_lineのTDD例:

// Step 1: Red(テストを書く)
void test_empty_file(void)
{
    int fd = open("empty.txt", O_RDONLY);
    char *line = get_next_line(fd);
    assert(line == NULL);  // 空ファイルはNULL
    close(fd);
}

// Step 2: Green(最小限の実装)
char *get_next_line(int fd)
{
    // まずは空ファイルのケースだけ通す
    return (NULL);
}

// Step 3: Refactor(改善)
// ... 他のテストケースを追加しながら実装を拡張 ...

5.6 get_next_lineのアーキテクチャ設計

5.6.1 階層設計

前述の理論を適用したget_next_lineの設計:

┌─────────────────────────────────────────────────────────────┐
│                     Layer 4: Public API                     │
│                                                             │
│   char *get_next_line(int fd)                              │
│   ────────────────────────────────────────────────────     │
│   契約:                                                     │
│     事前: fd >= 0, BUFFER_SIZE > 0                         │
│     事後: 1行を返す(改行含む)または NULL                    │
│                                                             │
└───────────────────────────┬─────────────────────────────────┘
                            │
    ┌───────────────────────┼───────────────────────┐
    ▼                       ▼                       ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Layer 3: Core   │ │ Layer 3: Core   │ │ Layer 3: Core   │
│                 │ │                 │ │                 │
│ read_and_store  │ │ extract_line    │ │ update_remainder│
│ ───────────────│ │ ───────────────│ │ ───────────────│
│ 責任:           │ │ 責任:           │ │ 責任:           │
│ ファイルから    │ │ バッファから    │ │ 使用済み部分    │
│ 読み取り蓄積    │ │ 1行を抽出      │ │ を削除          │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
         │                   │                   │
═════════╧═══════════════════╧═══════════════════╧═════════════
                    抽象化障壁
═══════════════════════════════════════════════════════════════
         │                   │                   │
┌────────┴────────┐ ┌────────┴────────┐ ┌────────┴────────┐
│ Layer 2: Utils  │ │ Layer 2: Utils  │ │ Layer 2: Utils  │
│                 │ │                 │ │                 │
│ ft_strjoin      │ │ ft_strchr       │ │ ft_substr       │
│ ───────────────│ │ ───────────────│ │ ───────────────│
│ 2つの文字列を   │ │ 文字を検索     │ │ 部分文字列を    │
│ 結合           │ │                 │ │ 抽出           │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
         │                   │                   │
═════════╧═══════════════════╧═══════════════════╧═════════════
                    抽象化障壁
═══════════════════════════════════════════════════════════════
                            │
                    ┌───────┴───────┐
                    │ Layer 1: Basic │
                    │                │
                    │   ft_strlen    │
                    │ ──────────────│
                    │ 文字列長を計算 │
                    └───────────────┘

5.6.2 ファイル構成

get_next_line/
├── get_next_line.h          # インターフェース定義
│   ├── インクルードガード
│   ├── システムヘッダ
│   ├── BUFFER_SIZEマクロ
│   └── 関数プロトタイプ
│
├── get_next_line.c          # コア実装
│   ├── read_and_store()     # static
│   ├── extract_line()       # static
│   ├── update_remainder()   # static
│   └── get_next_line()      # extern
│
└── get_next_line_utils.c    # ユーティリティ
    ├── ft_strlen()
    ├── ft_strchr()
    ├── ft_strjoin()
    └── ft_substr()

5.7 実装: ユーティリティ層(Layer 1-2)

5.7.1 ft_strlen - 基盤関数

/**
 * ft_strlen - 文字列の長さを計算する
 *
 * 理論的背景:
 *   C言語の文字列はヌル終端('\0'で終わる)
 *   長さ = 先頭から'\0'までの文字数
 *
 * 契約:
 *   事前条件: s ≠ NULL ∧ sはヌル終端文字列
 *   事後条件: 戻り値 ≥ 0 ∧ s[戻り値] = '\0'
 *
 * 計算量: O(n)
 * 空間量: O(1)
 */
size_t ft_strlen(const char *s)
{
    size_t len;

    len = 0;
    while (s[len])
        len++;
    return (len);
}

5.7.2 ft_strchr - 文字検索

/**
 * ft_strchr - 文字列中の文字を検索する
 *
 * 理論的背景:
 *   線形探索アルゴリズム
 *   最初に一致する位置を返す
 *
 * 契約:
 *   事前条件: s ≠ NULL
 *   事後条件:
 *     c ∈ s ⟹ 戻り値は最初の出現位置へのポインタ
 *     c ∉ s ∧ c ≠ '\0' ⟹ 戻り値 = NULL
 *     c = '\0' ⟹ 戻り値は終端のポインタ
 *
 * 計算量: O(n)
 * 空間量: O(1)
 */
char *ft_strchr(const char *s, int c)
{
    char ch;

    ch = (char)c;
    while (*s)
    {
        if (*s == ch)
            return ((char *)s);
        s++;
    }
    if (ch == '\0')
        return ((char *)s);
    return (NULL);
}

5.7.3 ft_strjoin - 文字列結合

/**
 * ft_strjoin - 2つの文字列を結合する
 *
 * 理論的背景:
 *   新しいメモリ領域に両方の文字列をコピー
 *   元の文字列は変更しない(非破壊的操作)
 *
 * 契約:
 *   事前条件: s1 ≠ NULL ∧ s2 ≠ NULL
 *   事後条件:
 *     戻り値 ≠ NULL ⟹
 *       strlen(戻り値) = strlen(s1) + strlen(s2)
 *       ∧ 戻り値は新規確保メモリ
 *     戻り値 = NULL ⟹ malloc失敗
 *
 * メモリ責任: 呼び出し側がfree()
 *
 * 計算量: O(n + m) where n = len(s1), m = len(s2)
 * 空間量: O(n + m)
 */
char *ft_strjoin(char const *s1, char const *s2)
{
    char    *joined;
    size_t  i;
    size_t  j;

    if (!s1 || !s2)
        return (NULL);
    joined = malloc(ft_strlen(s1) + ft_strlen(s2) + 1);
    if (!joined)
        return (NULL);
    i = 0;
    while (s1[i])
    {
        joined[i] = s1[i];
        i++;
    }
    j = 0;
    while (s2[j])
    {
        joined[i + j] = s2[j];
        j++;
    }
    joined[i + j] = '\0';
    return (joined);
}

5.7.4 ft_substr - 部分文字列抽出

/**
 * ft_substr - 文字列から部分文字列を抽出する
 *
 * 理論的背景:
 *   配列スライシング操作
 *   s[start:start+len] に相当
 *
 * 契約:
 *   事前条件: なし(NULLチェックあり)
 *   事後条件:
 *     s = NULL ⟹ 戻り値 = NULL
 *     start ≥ strlen(s) ⟹ 戻り値 = ""
 *     それ以外 ⟹ 戻り値 = s[start:min(start+len, strlen(s))]
 *
 * 計算量: O(len)
 * 空間量: O(len)
 */
char *ft_substr(char const *s, unsigned int start, size_t len)
{
    char    *substr;
    size_t  s_len;
    size_t  actual_len;
    size_t  i;

    if (!s)
        return (NULL);
    s_len = ft_strlen(s);
    if (start >= s_len)
    {
        substr = malloc(1);
        if (substr)
            substr[0] = '\0';
        return (substr);
    }
    actual_len = s_len - start;
    if (actual_len > len)
        actual_len = len;
    substr = malloc(actual_len + 1);
    if (!substr)
        return (NULL);
    i = 0;
    while (i < actual_len)
    {
        substr[i] = s[start + i];
        i++;
    }
    substr[i] = '\0';
    return (substr);
}

5.7.5 完全なget_next_line_utils.c

/* ************************************************************************** */
/*                                                                            */
/*   get_next_line_utils.c - ユーティリティ関数群                              */
/*                                                                            */
/*   設計原則:                                                                 */
/*   - 各関数は単一責任                                                        */
/*   - 契約による設計(事前/事後条件を明確に)                                  */
/*   - 防御的プログラミング(NULL検査)                                        */
/*                                                                            */
/* ************************************************************************** */

#include "get_next_line.h"

size_t ft_strlen(const char *s)
{
    size_t len;

    len = 0;
    while (s[len])
        len++;
    return (len);
}

char *ft_strchr(const char *s, int c)
{
    char ch;

    ch = (char)c;
    while (*s)
    {
        if (*s == ch)
            return ((char *)s);
        s++;
    }
    if (ch == '\0')
        return ((char *)s);
    return (NULL);
}

char *ft_strjoin(char const *s1, char const *s2)
{
    char    *joined;
    size_t  i;
    size_t  j;

    if (!s1 || !s2)
        return (NULL);
    joined = malloc(ft_strlen(s1) + ft_strlen(s2) + 1);
    if (!joined)
        return (NULL);
    i = 0;
    while (s1[i])
    {
        joined[i] = s1[i];
        i++;
    }
    j = 0;
    while (s2[j])
    {
        joined[i + j] = s2[j];
        j++;
    }
    joined[i + j] = '\0';
    return (joined);
}

char *ft_substr(char const *s, unsigned int start, size_t len)
{
    char    *substr;
    size_t  s_len;
    size_t  actual_len;
    size_t  i;

    if (!s)
        return (NULL);
    s_len = ft_strlen(s);
    if (start >= s_len)
    {
        substr = malloc(1);
        if (substr)
            substr[0] = '\0';
        return (substr);
    }
    actual_len = s_len - start;
    if (actual_len > len)
        actual_len = len;
    substr = malloc(actual_len + 1);
    if (!substr)
        return (NULL);
    i = 0;
    while (i < actual_len)
    {
        substr[i] = s[start + i];
        i++;
    }
    substr[i] = '\0';
    return (substr);
}

5.8 実装: コア層(Layer 3-4)

5.8.1 read_and_store - ファイル読み取りと蓄積

/**
 * read_and_store - ファイルから読み取り、remainderに蓄積する
 *
 * 理論的背景:
 *   生産者-消費者パターン(第4章参照)
 *   read()がデータを生産、remainder が消費バッファ
 *
 * 状態遷移:
 *   remainder(old) + read(buffer) → remainder(new)
 *
 * 契約:
 *   事前条件: fd ≥ 0 ∧ BUFFER_SIZE > 0
 *   事後条件:
 *     成功 ⟹ 戻り値はremainderに新データを追加したもの
 *            ∧ 改行を含むか、EOF到達
 *     失敗 ⟹ 戻り値 = NULL ∧ メモリ解放済み
 */
static char *read_and_store(int fd, char *remainder)
{
    char    *buffer;
    char    *temp;
    ssize_t bytes_read;

    buffer = malloc(BUFFER_SIZE + 1);
    if (!buffer)
        return (NULL);
    bytes_read = 1;
    while (bytes_read > 0 && !ft_strchr(remainder, '\n'))
    {
        bytes_read = read(fd, buffer, BUFFER_SIZE);
        if (bytes_read == -1)
        {
            free(buffer);
            free(remainder);
            return (NULL);
        }
        buffer[bytes_read] = '\0';
        temp = ft_strjoin(remainder, buffer);
        free(remainder);
        remainder = temp;
    }
    free(buffer);
    return (remainder);
}

設計の解説:

read_and_store のフロー:

┌─────────────────┐
│     開始        │
└────────┬────────┘
         ▼
┌─────────────────┐
│ バッファ確保    │ malloc(BUFFER_SIZE + 1)
└────────┬────────┘
         ▼
┌─────────────────┐     ┌─────────────┐
│ remainderに     │ No  │             │
│ 改行がある?     │────→│ read()実行  │
└────────┬────────┘     └──────┬──────┘
         │                     │
         │ Yes                 ▼
         │               ┌─────────────┐
         │         No    │ bytes > 0?  │
         │       ┌───────┴─────────────┘
         │       │             │ Yes
         ▼       ▼             ▼
┌─────────────────┐     ┌─────────────┐
│ バッファ解放    │     │ 文字列結合  │
└────────┬────────┘     └──────┬──────┘
         ▼                     │
┌─────────────────┐            │
│ remainder返却   │←───────────┘
└─────────────────┘

5.8.2 extract_line - 行の抽出

/**
 * extract_line - remainderから1行を抽出する
 *
 * 理論的背景:
 *   文字列分割操作
 *   区切り文字('\n')までを取り出す
 *
 * 契約:
 *   事前条件: なし(NULLチェックあり)
 *   事後条件:
 *     remainder = NULL ∨ remainder = "" ⟹ 戻り値 = NULL
 *     改行あり ⟹ 戻り値 = 先頭から改行まで(改行含む)
 *     改行なし ⟹ 戻り値 = remainder全体
 */
static char *extract_line(char *remainder)
{
    char    *line;
    char    *newline_pos;
    size_t  len;

    if (!remainder || !*remainder)
        return (NULL);
    newline_pos = ft_strchr(remainder, '\n');
    if (newline_pos)
    {
        len = newline_pos - remainder + 1;
        line = ft_substr(remainder, 0, len);
    }
    else
    {
        line = ft_substr(remainder, 0, ft_strlen(remainder));
    }
    return (line);
}

ポインタ演算の説明:

remainder = "Hello\nWorld"
             ^    ^
             |    |
             |    newline_pos
             remainder

len = newline_pos - remainder + 1
    = 5 - 0 + 1
    = 6  ("Hello\n" の長さ)

ft_substr(remainder, 0, 6) → "Hello\n"

5.8.3 update_remainder - 残りの更新

/**
 * update_remainder - 使用済み部分を削除し、残りを返す
 *
 * 理論的背景:
 *   メモリ管理と所有権の移転
 *   古いremainderを解放し、新しいremainderを返す
 *
 * 契約:
 *   事前条件: なし
 *   事後条件:
 *     改行あり ⟹ 戻り値 = 改行以降の文字列
 *                ∧ 古いremainder解放済み
 *     改行なし ⟹ 戻り値 = NULL ∧ 古いremainder解放済み
 *
 * 所有権: 戻り値(またはNULL)が新しい所有者
 */
static char *update_remainder(char *remainder)
{
    char    *new_remainder;
    char    *newline_pos;
    size_t  start;

    newline_pos = ft_strchr(remainder, '\n');
    if (!newline_pos)
    {
        free(remainder);
        return (NULL);
    }
    start = newline_pos - remainder + 1;
    new_remainder = ft_substr(remainder, start, ft_strlen(remainder) - start);
    free(remainder);
    return (new_remainder);
}

5.8.4 get_next_line - メイン関数

/**
 * get_next_line - ファイルから1行を読み取る
 *
 * 理論的背景:
 *   状態機械(State Machine)パターン
 *   静的変数で呼び出し間の状態を保持
 *
 * 契約:
 *   事前条件: fd ≥ 0 ∧ BUFFER_SIZE > 0
 *   事後条件:
 *     行あり ⟹ 戻り値 = 次の1行(改行含む、またはEOF前の残り)
 *     EOF ⟹ 戻り値 = NULL
 *     エラー ⟹ 戻り値 = NULL
 *
 * 副作用:
 *   - fdのファイル位置が進む
 *   - 静的変数remainderが更新される
 *
 * メモリ責任:
 *   呼び出し側が戻り値をfree()
 */
char *get_next_line(int fd)
{
    static char *remainder;
    char        *line;

    if (fd < 0 || BUFFER_SIZE <= 0)
        return (NULL);
    remainder = read_and_store(fd, remainder);
    if (!remainder)
        return (NULL);
    line = extract_line(remainder);
    remainder = update_remainder(remainder);
    return (line);
}

5.8.5 完全なget_next_line.c

/* ************************************************************************** */
/*                                                                            */
/*   get_next_line.c - ファイルから1行ずつ読み取る                             */
/*                                                                            */
/*   設計原則:                                                                 */
/*   - 情報隠蔽: 静的関数で実装詳細を隠す                                      */
/*   - 単一責任: 各関数は1つの明確な役割                                       */
/*   - 契約による設計: 事前/事後条件を明確に定義                               */
/*                                                                            */
/* ************************************************************************** */

#include "get_next_line.h"

static char *read_and_store(int fd, char *remainder)
{
    char    *buffer;
    char    *temp;
    ssize_t bytes_read;

    buffer = malloc(BUFFER_SIZE + 1);
    if (!buffer)
        return (NULL);
    bytes_read = 1;
    while (bytes_read > 0 && !ft_strchr(remainder, '\n'))
    {
        bytes_read = read(fd, buffer, BUFFER_SIZE);
        if (bytes_read == -1)
        {
            free(buffer);
            free(remainder);
            return (NULL);
        }
        buffer[bytes_read] = '\0';
        temp = ft_strjoin(remainder, buffer);
        free(remainder);
        remainder = temp;
    }
    free(buffer);
    return (remainder);
}

static char *extract_line(char *remainder)
{
    char    *line;
    char    *newline_pos;
    size_t  len;

    if (!remainder || !*remainder)
        return (NULL);
    newline_pos = ft_strchr(remainder, '\n');
    if (newline_pos)
    {
        len = newline_pos - remainder + 1;
        line = ft_substr(remainder, 0, len);
    }
    else
        line = ft_substr(remainder, 0, ft_strlen(remainder));
    return (line);
}

static char *update_remainder(char *remainder)
{
    char    *new_remainder;
    char    *newline_pos;
    size_t  start;

    newline_pos = ft_strchr(remainder, '\n');
    if (!newline_pos)
    {
        free(remainder);
        return (NULL);
    }
    start = newline_pos - remainder + 1;
    new_remainder = ft_substr(remainder, start, ft_strlen(remainder) - start);
    free(remainder);
    return (new_remainder);
}

char *get_next_line(int fd)
{
    static char *remainder;
    char        *line;

    if (fd < 0 || BUFFER_SIZE <= 0)
        return (NULL);
    remainder = read_and_store(fd, remainder);
    if (!remainder)
        return (NULL);
    line = extract_line(remainder);
    remainder = update_remainder(remainder);
    return (line);
}

5.9 ヘッダファイル設計

/* ************************************************************************** */
/*                                                                            */
/*   get_next_line.h - インターフェース定義                                    */
/*                                                                            */
/*   設計原則:                                                                 */
/*   - 公開インターフェースのみを宣言                                          */
/*   - 実装詳細(static関数)は隠蔽                                            */
/*   - インクルードガードで二重定義防止                                        */
/*                                                                            */
/* ************************************************************************** */

#ifndef GET_NEXT_LINE_H
# define GET_NEXT_LINE_H

# include <stdlib.h>
# include <unistd.h>

/* コンパイル時に上書き可能 */
# ifndef BUFFER_SIZE
#  define BUFFER_SIZE 42
# endif

/* 公開インターフェース */
char    *get_next_line(int fd);

/* ユーティリティ関数 */
size_t  ft_strlen(const char *s);
char    *ft_strchr(const char *s, int c);
char    *ft_strjoin(char const *s1, char const *s2);
char    *ft_substr(char const *s, unsigned int start, size_t len);

#endif

5.10 テスト戦略

5.10.1 単体テストの設計

/*
 * テストフレームワーク(簡易版)
 */
#include <stdio.h>
#include <string.h>
#include <assert.h>

#define TEST(name) void test_##name(void)
#define RUN_TEST(name) do { \
    printf("Running %s... ", #name); \
    test_##name(); \
    printf("PASSED\n"); \
} while(0)

/* ft_strlen のテスト */
TEST(strlen_empty)
{
    assert(ft_strlen("") == 0);
}

TEST(strlen_normal)
{
    assert(ft_strlen("hello") == 5);
}

TEST(strlen_with_null)
{
    assert(ft_strlen("hel\0lo") == 3);  // ヌル文字で終了
}

/* ft_strchr のテスト */
TEST(strchr_found)
{
    const char *s = "hello";
    char *p = ft_strchr(s, 'l');
    assert(p == s + 2);
}

TEST(strchr_not_found)
{
    assert(ft_strchr("hello", 'z') == NULL);
}

TEST(strchr_null_char)
{
    const char *s = "hello";
    char *p = ft_strchr(s, '\0');
    assert(p == s + 5);
}

/* ft_strjoin のテスト */
TEST(strjoin_normal)
{
    char *result = ft_strjoin("hello", " world");
    assert(strcmp(result, "hello world") == 0);
    free(result);
}

TEST(strjoin_empty_first)
{
    char *result = ft_strjoin("", "world");
    assert(strcmp(result, "world") == 0);
    free(result);
}

/* テスト実行 */
int main(void)
{
    RUN_TEST(strlen_empty);
    RUN_TEST(strlen_normal);
    RUN_TEST(strlen_with_null);
    RUN_TEST(strchr_found);
    RUN_TEST(strchr_not_found);
    RUN_TEST(strchr_null_char);
    RUN_TEST(strjoin_normal);
    RUN_TEST(strjoin_empty_first);
    printf("\nAll tests passed!\n");
    return 0;
}

5.10.2 統合テスト

#include "get_next_line.h"
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

void test_get_next_line(void)
{
    int fd;
    char *line;
    int line_num = 0;

    /* テストファイルの作成 */
    fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    write(fd, "Line 1\n", 7);
    write(fd, "Line 2\n", 7);
    write(fd, "Line 3", 6);  // 改行なし
    close(fd);

    /* テスト実行 */
    fd = open("test.txt", O_RDONLY);

    line = get_next_line(fd);
    assert(strcmp(line, "Line 1\n") == 0);
    free(line);

    line = get_next_line(fd);
    assert(strcmp(line, "Line 2\n") == 0);
    free(line);

    line = get_next_line(fd);
    assert(strcmp(line, "Line 3") == 0);
    free(line);

    line = get_next_line(fd);
    assert(line == NULL);  // EOF

    close(fd);
    unlink("test.txt");

    printf("Integration test passed!\n");
}

5.10.3 境界値テスト

void test_edge_cases(void)
{
    int fd;
    char *line;

    /* 空ファイル */
    fd = open("empty.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    close(fd);
    fd = open("empty.txt", O_RDONLY);
    line = get_next_line(fd);
    assert(line == NULL);
    close(fd);
    unlink("empty.txt");

    /* 改行のみ */
    fd = open("newline.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    write(fd, "\n", 1);
    close(fd);
    fd = open("newline.txt", O_RDONLY);
    line = get_next_line(fd);
    assert(strcmp(line, "\n") == 0);
    free(line);
    line = get_next_line(fd);
    assert(line == NULL);
    close(fd);
    unlink("newline.txt");

    /* 無効なfd */
    line = get_next_line(-1);
    assert(line == NULL);

    printf("Edge case tests passed!\n");
}

5.11 Valgrindによるメモリ検証

5.11.1 メモリリーク検出

# コンパイル(デバッグ情報付き)
gcc -g -D BUFFER_SIZE=42 \
    get_next_line.c get_next_line_utils.c main.c \
    -o gnl

# Valgrindで実行
valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         ./gnl test.txt

理想的な出力:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 0 bytes in 0 blocks
==12345==   total heap usage: 150 allocs, 150 frees, 5,432 bytes allocated
==12345==
==12345== All heap blocks were freed -- no leaks are possible

5.11.2 メモリエラーの検出

よくあるメモリエラー:

1. 不正な読み取り(Invalid read)
   原因: 解放済みメモリへのアクセス
   対策: ポインタをNULLに設定

2. 不正な書き込み(Invalid write)
   原因: バッファオーバーフロー
   対策: 境界チェック

3. 条件付きジャンプ(Conditional jump depends on uninitialised value)
   原因: 未初期化変数の使用
   対策: 変数を初期化

4. メモリリーク(definitely lost)
   原因: free忘れ
   対策: コードレビュー、静的解析

5.12 実装チェックリスト

5.12.1 機能要件

□ fd < 0 で NULL を返す
□ BUFFER_SIZE <= 0 で NULL を返す
□ 改行を含む行を正しく返す(改行を含める)
□ 改行なしのファイル(最後の行)を正しく返す
□ 空ファイルで NULL を返す
□ EOF後に NULL を返す
□ 連続した空行(\n\n\n)を正しく処理
□ 標準入力(fd = 0)で動作
□ 巨大なファイル(GB単位)で動作
□ 巨大な行(MB単位)で動作
□ バイナリデータ(\0を含む)で動作

5.12.2 メモリ管理

□ Valgrindでリークなし
□ すべてのmallocにNULLチェック
□ エラー時にすべてのメモリを解放
□ 二重freeが発生しない
□ 未初期化メモリへのアクセスなし

5.12.3 コード品質

□ 42 Norm準拠
□ 関数は25行以内
□ 各関数は単一責任
□ 契約(事前/事後条件)が明確
□ 防御的プログラミング適用

5.13 まとめ: 理論から実装へ

本章では、ソフトウェア工学の理論を土台にget_next_lineを実装しました。

学んだ計算機科学の概念:

1. モジュール設計(Parnas, 1972)
   → 情報隠蔽、抽象化障壁

2. 結合度と凝集度(Constantine & Yourdon, 1979)
   → 低結合・高凝集なモジュール分解

3. SOLID原則(Martin, 2000s)
   → 単一責任、開放閉鎖原則

4. 契約による設計(Meyer, 1986)
   → 事前条件、事後条件、不変条件

5. Hoare論理(Hoare, 1969)
   → プログラムの正しさの形式的定義

6. テスト理論
   → 境界値分析、同値分割、TDD

実装のキーポイント:

┌─────────────────────────────────────────────────────────────┐
│ get_next_lineの設計まとめ                                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│ アーキテクチャ: 4層のレイヤード設計                          │
│ モジュール化:   情報隠蔽(static関数)                       │
│ エラー処理:     防御的プログラミング + エラー伝播            │
│ メモリ管理:     所有権の明確化 + リークゼロ                  │
│ テスト:         境界値分析 + 同値分割                        │
│                                                             │
│ 結果: 堅牢で保守性の高い実装                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

次章では、ボーナスパート(複数ファイルディスクリプタの同時処理)を通じて、より高度な状態管理とデータ構造の使用について学びます。