ソースコードの可読性向上 第一回 テーマ:命名

目次
- 序文
- ソースコードの可読性向上テクニックを学ぶ意義
- 命名と基本方針
- アンチパターン
- 具体例
- まとめ
序文
皆さんこんにちは。 ミラクシアエッジテクノロジーの橘です。
ソフトウェアエンジニアの皆さんは日々ソースコードを読むことに多くの時間を使っているのではないでしょうか? わかりにくいコードを理解するために悪戦苦闘している方も少なくないでしょう。 私も「数百行の巨大関数」「理解不可能な変数名」「グローバル変数」などに苦しめられてきました。 本記事では、そういった状況を改善してエンジニアの生産性を向上するため、ソースコードの可読性を上げる方法やアンチパターンを共有していきます。 今回のテーマは”命名”です。 アンチパターンや具体例を用いて、命名改善によりソースコードの可読性が大きく改善されることを示します。 命名ひとつで、チーム全体の生産性が劇的に変わる――そんな”小さいけど大きな工夫”をはじめる助けになれば幸いです。
また今後、ネストを浅くする・適切なコメントをつけるなど、その他テーマについても解説していきます。
ソースコードの可読性向上テクニックを学ぶ意義
本題に入る前に、可読性向上の取り組みやそのための学習がなぜ重要なのか、いくつかの異なる観点から理由を説明します。
人間は複雑なこと、長いことを記憶・理解できないため
人の認知能力のリソースには限りがあります。 例えば短期記憶の容量(チャンク)は7±2程度(近年では4±1という説も)、集中力の持続時間は45分から15分程度だそうです。 (気になる方はマジカルナンバーなどで調べてみてください) 可読性が低いコードの読解には、これらのリソースを大量に消費します。 本来考えるべきことにリソースを割くためにも、ソースコードを容易に理解できるようにしておく必要があります。
エンジニアの活動全体に影響するため
不具合解析、新規機能追加、スクラッチ開発、新しいプロジェクトのキャッチアップなど、あらゆる活動でエンジニアはソースコードを読みます。 可読性の高いコードを書くことは、これらの活動すべての効率向上につながります。 しかも、プロジェクトに参加する全メンバーにです。 可読性向上はプロジェクト効率向上にとって効果的な取り組みと言えるでしょう。
学習により習得可能な技術であるため
私の知る限り、複雑なコードを容易に理解する技術は存在しません。 一方、読みやすいコードを書く技術は学習を通して習得可能です。 前者で悪戦苦闘しても、技術として身につかない可能性が高いということです。 この点からも、可読性向上のコストパフォーマンスの高さがわかるかと思います。
命名と基本方針
関数や変数に適切な名前が付いたコードは非常に理解しやすくなります。 コード理解に必要な情報を名前から得られるためです。 逆に不適切な命名がされている場合、コメントやドキュメントなしでコードを理解することは困難になります。 (もちろん名前が優れていればドキュメントやコメントを書かなくて良いということではありません)
ソースコードへの理解の曖昧さ、誤解の可能性を最小にすること、そのために名前から得られる情報量を最大化することを目標とします。 目標達成のため、守るべき方針は以下2つに集約されると考えています。
- 汎用的・抽象的な名前より具体的な名前を付ける
- 実態に合った名前を付ける
また、プロジェクトのコーディング規約に従うというのも重要な観点です。 もちろん、コーディング規約が有益なものである必要があります。 社内でのコーディング規約の整備が進んでいないならば、googleのスタイルガイドなどを参考にしてみてください。
ではどうすればこの方針を達成できるのか、アンチパターンを通じて考えてみましょう。
アンチパターン(汎用的・抽象的な命名)
末尾に番号が付いた名前
経験上このケースはまれですが、変数の末尾に数値を付けた名前を見ることがあります。
int num1 = num2 = 0;
num1、num2が何のデータを保持するのか変数名から理解できません。 1や2といった数値が何の情報も伝えてくれないためです(もちろんnumも)。 単語 + 番号という名前は避けてください。 関数名も同じです。仮に1→2→3の順で実行するといった時間関係がある場合でも、関数名の末尾に1、2、3と付けるのは避けましょう。
int message_header_len = 0; //改善例 : messageに関するデータとわかる名前に変更
int message_body_len = 0;
抽象的な名前を使う
次の例ではsend_message()の戻り値を変数retに代入しています。 このretという名前は戻り値を格納する以外の情報を持ちません。
int ret = send_message(message);
send_message()がどのような値を返すかがわかる変数名を検討しましょう。 転送したバイト数ならtransferred_bytes、HTTP Status codeならhttp_status_codeなど、具体的な名前を付けることで情報量を増やすことが可能です。
int http_status_code = send_message(message); //改善例 : 抽象的なretから、具体的な名前に変更
ただし以下のように小さいスコープでの使用するならばretでも問題ないでしょう。
int ret = send_message(message);
if (ret != E_OK) {
エラー処理
return -1;
}
return 0;
抽象的な名前を使う(ループカウンタ)
多くの場合、ループカウンタにはiやjを使います。 簡単なループならば問題ありませんが、複雑な処理を行う多重ループでは理解の妨げとなり、不具合の原因にもなります。
int scores[3][5] = {
{70, 80, 90, 85, 88},
{78, 82, 87, 90, 92},
{85, 89, 91, 93, 95}
};
for (int i = 0; i < 3; i++) {
int total_score = 0;
for (int j = 0; j < 5; j++) {
total_score += scores[i][j]; //アンチパターン : iとjを誤って使う可能性もある
}
printf("Student %d total score: %d\n", i + 1, total_score);
}
return 0;
その場合はループカウンタに具体的な名前を付けることを検討します。 以下改善例を示します。
int scores[3][5] = {
{70, 80, 90, 85, 88},
{78, 82, 87, 90, 92},
{85, 89, 91, 93, 95}
};
for (int student = 0; student < 3; student++) {
int total_score = 0;
for (int subject = 0; subject < 5; subject++) {
totalScore += scores[student][subject]; //改善例 : indexの変数を具体化し、誤用を防ぐ
}
printf("Student %d total score: %d\n", student + 1, total_score);
}
return 0;
外側のループが生徒を、内側のループが教科を処理することが分かりやすくなりました。 実開発でも、あるループが何を目的に行われているのか迷うケースは少なくありません。 そんなとき、適切な名前のループカウンタは理解のヒントになります。
ただし、そもそも多重ループの使用は極力避けるべきであるという点は覚えておいてください。 そのことについては後日、ネストを浅くするというテーマで解説します。
また、上記例ではfor文の初期化式で変数定義をしています。 これはC99以降で使用できる記載方法で、変数のスコープをブロック内に限定できます。
省略形を使う(独自の省略形)
int mh = read_buffer();
mhは何の略でしょうか?max_height?それともmodule_hash? 私の経験上、ほとんどのケースで省略形を使うと、読み手に伝えるべき情報量が激減します。 strやnumあるいはIPv4やUMLなど、一般的に使われている省略形以外は使わない方が良いでしょう。
int module_hash = read_buffer(); //改善例 : 意味不明の省略形から非省略形に変更
省略形を使う(一文字の省略形)
struct module_instance *m = g_module_instance;
1文字の省略形も時々見かけます。 変数名の長さに制約があった時代の名残かもしれません。 現代ではほとんどの環境でそのような制限はなくなっていますので、こういった短縮形を用いるのは避けましょう。
struct module_instance *module_instance = g_module_instance; //改善例 : 意味不明の省略形から非省略形に変更
アンチパターン(実態と異なる命名)
実態と異なる名前を使う
例えば以下のような名前の公開関数がヘッダに定義されていたと仮定しましょう。
int send_message(const char* message, const char* url);
この関数を見たエンジニアは、受け渡したmessageをサーバに送信するものと考えるでしょう。 しかし実際にコードは以下のようになっているとします。
int send_message(const char* message, const char* url) {
事前処理
enqueue_message(message, url);
エラー処理など
}
つまりこの関数はmessageをキューイングするだけで送信まで行っていません。 最終的にメッセージをサーバに送信するという点において、send_messageという名前は実態と一致しているため致命的ではありませんが、混乱を招く名前ではあります。 register_message_send_queue()など実態に合った名前を検討しましょう。
int register_message_send_queue(const char* message, const char* url); //改善例 : 実態に合った関数名に変更
ここまでいくつかのアンチパターンを示してきました。 名前から得られる情報量を増やすことのイメージがつかめたでしょうか。
具体例
ではある程度まとまった分量のコードで例を見てみましょう。 温度センサーから1分間隔でデータを読み取り、1時間の平均をレポートするというサンプルです。 コードのコメントは命名規則の解説を行うために記載しているので、本来このようなコメントを残すべきではありません。 有用なコメントがどのようなものかについては、また別の機会に紹介します。 またbeforeはdefine定数を使うべきところをマジックナンバーにしています。 サンプルということでご容赦ください。
before
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#define SAMPLE_COUNT 60
double temp_values[SAMPLE_COUNT];
double read_data(void) {
return (20.0 + (rand() % 1000) / 100.0);
}
double calc(void) {
double st = 0.0;
int vs = 0;
for (int i = 0; i < SAMPLE_COUNT; i++) {
if(temp_values[i] != 5000.0) {
st += temp_values[i];
vs++;
}
}
return vs > 0 ? ste / vs : 0.0;
}
void report(const double at) {
fprintf(stdout, "avg: %.2f\n", at);
}
int main(void) {
srand((unsigned int)time(NULL));
int count = 0;
while (1) {
sleep(60);
double mt = read_data();
if (mt < -50.0 || mt > 150.0) {
mt = 5000.0;
fprintf(stderr, "Invalid data !!\n");
}
temp_values[count] = mt;
count++;
if (count >= 60) {
double at = calc();
report(at);
count = 0;
}
}
return 0;
}
after
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
//修正 : マジックナンバーを名前付きの定数に変更
#define SAMPLES_PER_HOUR 60
#define MINUTE_SIMULATION_MS 100
#define MIN_VALID_TEMPERATURE -50.0
#define MAX_VALID_TEMPERATURE 150.0
#define INVALID_TEMPERATURE 5000.0
double temperature_samples[SAMPLES_PER_HOUR];
//修正 : 温度データをセンサーから読み取ることがわかる関数名に変更
double read_temperature_from_sensor(void) {
// 擬似センサー値:20.00〜30.00の範囲でランダム
return 20.0 + (rand() % 1000) / 100.0;
}
//修正 : 1時間の平均温度を計算することがわかる関数名に変更
double calculate_hourly_average_temperature(void) {
double sum_temperature = 0.0;
int valid_samples = 0;
//修正 : この程度のfor文ならばiでもOK、ここでは例としてindexを使用
for (int index = 0; index < SAMPLES_PER_HOUR; index++) {
if (temperature_samples[index] != INVALID_TEMPERATURE) {
sum_temperature += temperature_samples[index];
valid_samples++;
}
}
return valid_samples > 0 ? sum_temperature / valid_samples : 0.0;
}
//修正 : 1時間の平均気温レポートを出力することがわかる関数名に変更
void print_hourly_temperature_report(const double average_temperature) {
fprintf(stdout, "Hourly average temperature: %.2f C\n", average_temperature);
}
int main(void) {
srand((unsigned int)time(NULL));
int sample_count = 0;
while (1) {
sleep(60);
//修正 : 意味不明の省略形からより具体的な変数名に変更
double measured_temperature = read_temperature_from_sensor();
if (measured_temperature < MIN_VALID_TEMPERATURE ||
measured_temperature > MAX_VALID_TEMPERATURE) {
measured_temperature = INVALID_TEMPERATURE;
fprintf(stderr, "Invalid temperature !!\n");
}
temperature_samples[sample_count] = measured_temperature;
sample_count++;
if (sample_count >= SAMPLES_PER_HOUR) {
double average_temperature = calculate_hourly_average_temperature();
print_hourly_temperature_report(average_temperature);
sample_count = 0;
}
}
return 0;
}
おそらくbeforeは何をしているかわからないと思います。 一方afterでは、処理を説明するコメントがついていないにもかかわらず、 どのような計算を行っているか名前からわかるようになったのではないでしょうか。
まとめ
今回はプログラミングにおける命名について説明しました。 解説した点に注意し、より良い命名を考えるようにしましょう。 また、より良い命名をするためにはレビューが非常に重要な工程です。 実際この記事の第一稿では、”実態とは異なる名前”の改善案をregister_message_sender_queue()としていましたが、 register_message_send_queue()の方が良いだろうという指摘をいただけました。 レビュアーの意見を積極的に取り入れ、より分かりやすい名前を目指してください。
参考図書
- リーダブルコード より良いコードを書くためのシンプルで実践的なテクニック
- CODE COMPLETE 第2版 完全なプログラミングを目指して
- 良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方
- リファクタリング(第2版): 既存のコードを安全に改善する
採用情報
ミラクシアでは一緒に組込みソフト開発をする仲間を募集しています。

