第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関数) │
│ エラー処理: 防御的プログラミング + エラー伝播 │
│ メモリ管理: 所有権の明確化 + リークゼロ │
│ テスト: 境界値分析 + 同値分割 │
│ │
│ 結果: 堅牢で保守性の高い実装 │
│ │
└─────────────────────────────────────────────────────────────┘
次章では、ボーナスパート(複数ファイルディスクリプタの同時処理)を通じて、より高度な状態管理とデータ構造の使用について学びます。