僕がプログラムを分割したくなる時
みなさん、プログラミングをしていると「分割統治」という言葉をよく耳にすると思います。関数を分ける、ファイルを分ける、ディレクトリを分ける……「分割」にはいろんなレベルがあります。そしてその目的は一貫して、「問題のサイズを小さくし、扱いやすくすること」です。
人間って、大きくて複雑なものを一気に作るより、小さくて単純なものをいくつも扱うほうが得意な生き物なんです。だから、プログラムが「ちょっとややこしくなりそうだな」と感じたときこそ、「切り分けて、分けて、小さくしていく」チャンスです。
プログラマ視点での「分割統治」は、処理を“意味のある1塊”ごとに分けて、それぞれ独立して実装していくことを指します。その理由は先の通りですが、処理を分割するとはどういうことでしょうか。今回は、ちょっとしたサンプルを交えながら、この「分割」を体験していきます。
最近、C言語の学習をしているので、サンプルはC言語で作成していきます。
🔍 プログラムにおける「分割」のスケール感
「分割統治」と一口に言っても、その対象と目的はさまざまです。
ここでは、プログラミングにおける“分割”を レイヤー構造として可視化 してみましょう。
レベル | 分割対象 | 主な目的・メリット | 例 |
---|---|---|---|
🧩 ロジック | 関数(Function) | 処理を意味ごとの「1単位」にする。可読性・再利用性の向上。 |
get_encrypted_char() などの小関数 |
📄 モジュール | ファイル(File) | 関連する処理やデータ構造をまとめて責務を明確にする。 |
encrypt.c / encrypt.h
|
🗂 レイヤー | ディレクトリ | 層ごとに責務を分け、プロジェクト全体の見通しを良くする。 |
src/ , include/ , test/
|
📦 ライブラリ | 静的/動的ライブラリ | 再利用性の向上、分離ビルド、他プロジェクトでも使えるように独立化。 |
libencrypt.a , libutils.so など |
🧱 構成 | サービス単位(環境) | 実行環境ごとに役割を分離。開発・運用・拡張をしやすくする。 |
web / app / db (Docker構成など) |
本書ではロジックの分割とファイル分割そしてディレクトリ分割までを扱います。
ロジック分割を行いたくなるタイミング
サンプルとして、標準出力から体重、身長を入力してBMI値を求めるプログラムを考えます。これはC言語のプログラミング演習ではよくあるテーマで、皆さんどこかでやられたことがあるかもしれません。
通常はこのように書くでしょう。
#include <stdio.h>
int main(void){
double height;
double weight;
printf("身長を入力してください[cm]:");
height = scanf(" %fl",&height)/100;
printf("体重を入力してください[kg]:")
weight = scanf(" %fl",&weight);
double bmi = weight / (height*height);
printf ("BMI=%lf",bmi);
}
この程度のプログラムなら、別に分割する必要はない。分割欲求は低いです。
言語の勉強をしている間はこのようにプログラムが単純すぎて、分割する必要がないので、「分割して効率よく」という考え方にはなかなか至りません。
ところが、ここで一つ、機能を追加します。
- 入力値が想定するデータ型じゃない場合、(文字など) 再入力を促す。
この処理を先のコードに適用すると
#include <stdio.h>
int main(void){
double height;
double weight;
while(1){
printf("身長を入力してください[cm]:");
if(scanf(" %fl",&height) ==1){
break;
} else {
printf("値が正しく入力されていません。再度入力してください。");
// バッファをクリア
int ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
}
height /= 100;
while(1){
printf("体重を入力してください[kg]:")
if(scanf(" %fl",&weight)!=1){
break;
} else {
printf("値が正しく入力されていません。再度入力してください。");
// バッファをクリア
int ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
}
double bmi = weight / (height*height);
printf ("BMI=%lf",bmi);
}
こうなってくると、だんだんとわかってきます。まずは「同じような処理を何度も書いていると感じた時」に処理を分割したくなるのです。
📌 同じ処理が繰り返されていると感じたら、分割のサイン
コードを読んでみると、
- 入力を受け取る
- 入力が不正なら再入力を促す
- 入力バッファをクリアする
といった一連の流れが 体重と身長でほぼ同じように書かれている ことに気づきます。
このような「同じような処理を何度も書いている」という感覚は、関数で切り出すべき合図です。 処理の意味がはっきりしているので、関数化によって読みやすく、使いやすくなります。
✂️ 入力処理を関数に分割してみる
たとえば、以下のような関数にしてみましょう:
double get_double(char* message) {
double result;
while (1) {
printf("%s", message);
if (scanf("%lf", &result) == 1) {
break;
} else {
printf("値が正しく入力されていません。再度入力してください。\n");
int ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
}
return result;
}
main.cの内容は以下のようになります。
// main.c
#include <stdio.h>
double get_double(const char* message) {
double result;
while (1) {
printf("%s", message);
if (scanf("%lf", &result) == 1) {
break;
} else {
printf("値が正しく入力されていません。再度入力してください。\n");
// 入力バッファをクリア
int ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
}
return result;
}
int main(void) {
double height = get_double("身長を入力してください[cm]: ") / 100;
double weight = get_double("体重を入力してください[kg]: ");
double bmi = weight / (height * height);
printf("BMI = %.2lf\n", bmi);
return 0;
}
mainが一気にすっきりしましたね。せっかくなので、BMI値の計算も関数にします。
#include <stdio.h>
double get_double(const char* message) {
double result;
while (1) {
printf("%s", message);
if (scanf("%lf", &result) == 1) {
break;
} else {
printf("値が正しく入力されていません。再度入力してください。\n");
int ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
}
return result;
}
double calc_bmi(double height_m, double weight_kg) {
return weight_kg / (height_m * height_m);
}
int main(void) {
double height = get_double("身長を入力してください[cm]: ") / 100;
double weight = get_double("体重を入力してください[kg]: ");
double bmi = calc_bmi(height, weight);
printf("BMI = %.2lf\n", bmi);
return 0;
}
本当にすっきりしましたね。ここまでで体感できる、「分割」のメリットは
- 同じ処理を何度も書かなくてよくなる
- プログラムの見通しがよくなる
の2点です。ここでもう一つのメリットを体験してみましょう。
機能追加を行う
BMIの値によってメッセージを切り替える処理を実装します。
- BMI値が25以上で「あなたは肥満です」
- BMI値が25より小さい場合「あなたは肥満ではありません」
と記載します。calc_bmiがある場合とない場合で変更の仕方を見てみましょう。
📝 関数を使っていない場合の修正
まず、calc_bmi()
を使っていないコードで書くと、このようになります:
double height = get_double("身長を入力してください[cm]: ") / 100;
double weight = get_double("体重を入力してください[kg]: ");
double bmi = weight / (height * height);
printf("BMI = %.2lf\n", bmi);
if (bmi >= 25.0) {
printf("あなたは肥満です。\n");
} else {
printf("あなたは肥満ではありません。\n");
}
この場合、BMIの計算ロジックと表示処理が密結合になっています。ちょっとした変更であっても、main() の中を直接いじる必要があり、品質の担保が難しくないります。
✅ get_bmi_message() 関数を追加した構成
int is_fat(double bmi) {
return bmi >= 25.0;
}
const char* get_bmi_message(double bmi) {
if (is_fat(bmi)) {
return "あなたは肥満です。";
} else {
return "あなたは肥満ではありません。";
}
}
…
int main(void) {
double height = get_double("身長を入力してください[cm]: ") / 100;
double weight = get_double("体重を入力してください[kg]: ");
double bmi = calc_bmi(height, weight);
printf("BMI = %.2lf\n", bmi);
printf("%s\n", get_bmi_message(bmi));
return 0;
}
このように、条件分岐をget_bmi_message()
という関数にまとめることで、
main関数が 「何をしているか」に集中した読みやすい形になります。
今後、分岐のロジックを変更したくなったときも、main()
を変更する必要はなく、
get_bmi_message()
のみを直せばOKです。
「変化しそうな処理」と「安定している処理」を分ける これも関数分割の大事な視点のひとつです。そして関数の外に影響が漏れ出ないように差配することで、プログラムの品質はぐっと上がります。この「変更容易性の確保」も「分割」のメリットの一つです。
動作確認用のコードを追加する
みなさん。動作確認はどうしていますか?mainを分割していない人たちは、基本 gcc main.c
として./a.out
し、「実際に入力してみる」テストをするのだと思います。
せっかく関数分割をしていますので、簡単な動作確認用のコードを書いてみましょう。
#include <stdio.h>
#include "bmi.h"
#include <stdbool.h>
#include <string.h>
void test_calc_bmi() {
double bmi = calc_bmi(1.70, 72.25);
printf("test_calc_bmi: ");
if (bmi > 24.99 && bmi < 25.01) {
printf("OK\n");
} else {
printf("NG (bmi = %.2f)\n", bmi);
}
}
void test_is_fat() {
printf("test_is_fat: ");
if (is_fat(25.0) == true && is_fat(24.9) == false) {
printf("OK\n");
} else {
printf("NG\n");
}
}
void test_get_bmi_message() {
printf("test_get_bmi_message: ");
const char* msg1 = get_bmi_message(26.0);
const char* msg2 = get_bmi_message(23.0);
if (
msg1 != NULL && msg2 != NULL &&
strcmp(msg1, "あなたは肥満です。") == 0 &&
strcmp(msg2, "あなたは肥満ではありません。") == 0
) {
printf("OK\n");
} else {
printf("NG\n");
}
}
int main(void) {
test_calc_bmi();
test_is_fat();
test_get_bmi_message();
return 0;
}
さて、コンパイルして実行してみましょう。と。。思ったのですが、一つ問題が出てきました。gccの制限でmain関数を含むファイルは同時に2つ以上ビルドできない。
今回、プログラムの実態が入っているmain.cとテストコードを入れるtest.cの両方にmain関数があるため、構成を少し変更して上げる必要があります。
まず、最初にファイル構成です。ファイル構成は以下のようになります。
.
├── bmi.c
├── bmi.h
├── main.c
└── test.c
1. mainから関数を分離する。
#include <stdio.h>
#include "bmi.h"
int main(void) {
double height = get_double("身長を入力してください[cm]: ") / 100;
double weight = get_double("体重を入力してください[kg]: ");
double bmi = calc_bmi(height, weight);
printf("BMI = %.2lf\n", bmi);
printf("%s\n", get_bmi_message(bmi));
return 0;
}
2. ヘッダファイルを作成する。
次にbmi.hファイルに「関数の定義だけ」を定義します。.hファイルはヘッダファイルと言って、「分離された関数の呼び出し方」を定義しています。利用時にはこのbmi.hファイルをincludeして利用するのです。bmi.cではありません。注意してください。
#ifndef BMI_H
#define BMI_H
/**
* @brief ユーザーにプロンプトを表示し、double型の数値を標準入力から取得する。
*
* @param message 入力プロンプトとして表示する文字列
* @return 入力された double 値
*/
double get_double(const char* message);
/**
* @brief 身長と体重からBMI値を計算する。
*
* @param height_m 身長(メートル)
* @param weight_kg 体重(キログラム)
* @return 計算されたBMI値
*/
double calc_bmi(double height_m, double weight_kg);
/**
* @brief 指定したBMI値が「肥満」かどうかを判定する。
*
* @param bmi 対象のBMI値
* @return 肥満なら1、そうでなければ0
*/
int is_fat(double bmi);
/**
* @brief BMI値に応じた判定メッセージを返す。
*
* @param bmi 対象のBMI値
* @return 肥満かどうかを示すメッセージ文字列
*/
const char* get_bmi_message(double bmi);
#endif
3. 関数実装を作成するbmi.cを作成する。
bmi.cを用意し、関数の実態を記載します。
#include <stdio.h>
#include "bmi.h"
double get_double(const char* message) {
double result;
while (1) {
printf("%s", message);
if (scanf("%lf", &result) == 1) break;
printf("値が正しく入力されていません。再度入力してください。\n");
int ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}
return result;
}
double calc_bmi(double height_m, double weight_kg) {
return weight_kg / (height_m * height_m);
}
int is_fat(double bmi) {
return bmi >= 25.0;
}
const char* get_bmi_message(double bmi) {
return is_fat(bmi) ? "あなたは肥満です。" : "あなたは肥満ではありません。";
}
コンパイルと実行
$ gcc test.c bmi.c -o test_bmi
$ ./test_bmi
ヘッダの導入とソースコードを分割することで、関数のテストを実施することができるようになりました。これも「分割」の効能の一つです。
まとめ
今回、「関数分割」「ソースコード分割」という二つの分割を遠し、以下のメリットを体験しました。
- 同じ処理を何度も書かなくてよくなる
- プログラムの見通しがよくなる
- 変更容易性を確保しやすくなる
- テストしやすくなる。
反対にファイル分割が必要になるなどの面倒な点も発生しました。確かに面倒ですが、「分割」は品質と開発容易性の点で非常に有利な手法ですので、ぜひ活用してください。
次回は、より使いやすいディレクトリ構成を考察したいと思います。次回もお楽しみに。