はじめに
大学研究室(シミュレーション系)からIT業界に就職した新卒エンジニアが「これ面白いな~」「これ役立つな~」と思ったコーディングスタイルについて書いていきます。
コーディングスタイルに関する良書である『リーダブルコード』『Good Code, Bad Code』の内容をベースに、自分の経験や先輩エンジニアの言葉を入れて再編しています。
ここでは、コーディングを通じて守るべき方針や、命名法などの主に書き方に関するトピックを取り上げます。コード例は基本的にC#で書かれています。
コーディング方針編
具体的なコーディングスタイルに入る前に、その根底にある方針について述べたいと思います。
そもそも論:なぜ良いコーディングスタイルが重要なのか
シンプルに、「コードを使いやすくするため」です。「使いやすく」という言葉には色々な意味があります。可読性・保守性・堅牢さ・テストのしやすさ...
もしコードが「一度書いたら終わりで、誰も見ないし誰も再利用しない」なら、コーディングスタイルなんてどうでもよいかも知れません。
(研修室時代は割とこの思考でした...)
でも実際にはそうではありません。少なくとも今自分が書いたコードを後で見返すことは誰だってします。チーム開発なら自分が書いたコードを他の人が見るし、再利用することだっていくらでもあります。
そのときに、自分の書いたコードが下のような状態だったら困ります。とても。
- 変数が全て"a"とか"b"とかの適当なアルファベット
- main関数内に全ての処理が書いてある
- 同じような処理も繰り返しベタ書きしてある
- コメントが全くない
- クラス間に謎の依存関係があり、少し変更しただけで動かなくなる
将来の自分のため、または自分のコードを使う他の人のため、良いコーディングスタイルを意識しましょう。
理解にかかる時間を最短にする
理解にかかる時間が短ければ、使いやすいし、デバッグもしやすいし、変更を加えることも容易です。
というわけで、コードを書く際は「理解にかかる時間を最短にする」ことを心がけましょう。
巷でよく言われる「分かりやすい名前を付ける」などはこの最たるものですね。
基本的にはコードが短いほど理解にかかる時間も短いです。
コードが長いと純粋に見づらかったり、読むのに時間がかかります。
コードが長いということは、そのコードが冗長だったり、複雑すぎることの合図であることが多いです。コードは短くしましょう。
ただし、「コードはとにかく短ければよい」という考えは少し危険です。「短いけれど理解しにくい」コードが存在するためです。例えば下記のような(ChatGPTが作ってくれました)。
// 結局、何かしらの数字が返ってくるんだって。へぇ~。
var result = (new[] { 1, 2, 3, 4, 5 }.Select(x => x * x).Where(x => x % 2 == 0).Aggregate((a, b) => a + b) * new Func<int>(() => { var a = 10; var b = 20; return a + b; })() - (new Random().Next(1, 10) + 100) / 2);
こんなコード、読む気も失せますね。ええ。
同様に、関数やクラスを作らずに処理をベタ書きしたほうが行数としては短くなることもあるでしょう。
でも、それは「コードを使いやすくする」という理想には反しています。
行数も大切ですが、それよりは理解にかかる時間を短くすることにこだわりましょう。
レイヤーに分けて考える
私たちがコードを書く目的は何かしらの問題を解くことでしょう。その問題は複数の下位問題に分けられることがほとんどです。その下位問題もさらに下位の問題に分けられます。
このように、最終的に解きたい問題をレイヤーに分けて考えることで自然と読みやすく、使いやすい(再利用しやすい)コードになります。
また、分解した下位問題を解くためのライブラリがあるならば、それを自分のコードに再利用することだってできます。
例えば、「サーバにメッセージを送る」という上位問題を解くためには、「サーバへの通信を開く」「メッセージを送信する」「通信を閉じる」という下位問題を解きます。
これらの下位問題を解決する処理は自分で書くこともできますし、誰かが作ったライブラリを利用することもできます。ライブラリを利用すれば、各下位問題の詳細(通信はどう確立するか、エラー処理はどうするか...)を気にすることなく上位問題を解けますね。
using SomeLibrary;
public static void Main(string[] args)
{
bool hasSentMessage = DeliverMessageToServer(url: "some URL", message: "Hello!");
}
/// <summary>
/// サーバにメッセージを送信する。
/// </summary>
/// <param name="url">サーバのURL</param>
/// <param name="message">メッセージ</param>
private static bool DeliverMessageToServer(string url, string message)
{
try
{
SomeLibrary.Connect(url);
SomeLibrary.Send(message);
SomeLibrary.Close();
}
catch (ex 何かしらエラー)
{
return false;
}
return true;
}
逆に、main関数に自力で書いた全ての処理を詰め込むことも可能です。
しかし!この場合、同じような処理があったときに何か所にも重複して書かないといけないし、そもそも処理の全体像が把握しづらすぎます。
処理内容を変更する必要が生じたときも、該当箇所を頑張って探し出さなければなりません。しかも、その変更によってどこにどのような影響が出るかも分からないでしょう。
public static void Main(string[] args)
{
string url = "some URL";
string message = "Hello!";
// 接続を開く処理
// DNSを利用してIPアドレスを得る
// TCP接続を確立する
...
// メッセージを送信する処理
...
// 接続を閉じる処理
...
}
まずは、解決したい問題をレイヤーとしてとらえ、下位問題を解く処理に分けて考えましょう。処理を分けておけば簡単に再利用できます。
また、特定の下位問題の解き方を変更することも容易です。その箇所だけをそっくりそのまま入れ替えれば良いのです(上の例で言えば、ConnectをIPアドレス直指定で接続する関数に入れ替えるとか)。
レイヤーに分かれているので、他の箇所への影響も最小限です。いいことだらけですね。
「コードでの契約」を行う
自分の書いたコードが正しく使われることを保証することは「コードでの契約」と呼ばれます。
そして、コードでの契約においてある意味最も信頼できるのは、プログラミング言語の文法でありコンパイラです。
信頼できるコードでの契約の例です。
- 引数に正しい型を指定しておけば、それ以外の型の引数はコンパイルエラーとなる
- ある関数を呼ぶときに満たされているべき条件をチェックするようにすれば、誤った条件で呼ばれたときにエラーとなる
- 変数・関数・クラスの名前を無視することは(ひねった方法を使わない限り)できない。名前が分かりやすければ、書くコードも自然と分かりやすくなる
逆に、関数の挙動をコメントで補足することもできます(「この関数を呼ぶ前に○○してね」など)。
極端な話、引数も戻り値も全て任意にして、コメントでのみ特定の使い方を指示することも可能です。
でも、コメントって読まれないこともありますよね。また、仮に読んだとしても、その意図を100%汲み取ることは困難です。
その意味で、自分の書いたコードを正しく使ってもらう手段としてコメントは少し不確実です。
理想を言えば、コメントがほとんど無くても理解でき、使い方がわかるようなコードであれば確実に正しく使ってもらえると思います。
いわば、「コードが最も正確なドキュメントである」状態です。
ちなみにコードでの契約の度合を測る指標として、私は「コードをそのまま読み上げたとき、それを耳だけで第三者が理解できるか」という指標を先輩エンジニアに教わりました。
コーディング規約に従う
色々書きましたが、まず大切なのは「チームで採用しているコーディング規約に従う」ことでしょう。
これも結局は「(チーム内で)コードを使いやすくするため」です。よっぽど変な規約でない限りは従った方が無難です。
ほかの人はプライベートフィールドをcamelCaseで書いているのに、自分だけPascalCaseで書いていたら誤解のもとです。
コーディング規約の範疇でコードを分かりやすくしましょう。
命名編
わかりやすい名前にする
当たり前すぎますが、とても大事なことです。下のようなコードを考えましょう。
internal class G
{
private int S;
public G(int s)
{
S = s;
}
public void D()
{
Console.Write(S);
}
}
どう考えても読めません。理解できません。クソコードにもほどがあります
具体的かつ明瞭な命名をしましょう。
「この変数はこういう意味だよ~」「この関数はこういう処理をするよ~」という気持ちを込めるのが大事だそうです。
どのような英単語を選ぶか迷ったときはChatGPTに聞くといい感じにしてくれます。
internal class Game
{
private int Score;
public Game(int score)
{
Score = score;
}
public void DisplayGameScore()
{
Console.Write(Score);
}
}
下のコードのようにコメントで補足するのも悪くないです。
悪くはないのですが、コードでの契約の原則を考えると、やはり命名そのものを分かりやすくすべきでしょう(コメントも必要なら書いた方がいいと思います)。
クラス内のコメントで補足しても、呼び出し先ではやはりGとかDとか書かないといけません。
internal class G
{
/// <summary>
/// スコアを表す。
/// </summary>
private int S;
/// <summary>
/// コンストラクタでスコアの初期化を行う。
/// </summary>
/// <param name="s">スコア</param>
public G(int s)
{
S = s;
}
/// <summary>
/// スコアを出力する。
/// </summary>
public void D()
{
Console.Write(S);
}
}
誤解されない名前にする
裏を返せば、「読み手の期待に沿った名前にする」ということです。
例えば、"GetXxx"という関数を見たとき、読み手は何を期待するでしょう。おそらく、「関数を呼んだらすぐに値が戻ってくる」ことを期待するでしょう。
しかし、もしその処理に数分かかったら?嫌ですね。
double GetMeanScore()
{
// 実はこの中で平均の計算が走る
// 場合によっては数分かかる
// 読み手は怒る
}
「この関数の実行には時間がかかります」という製作者の声を関数名に入れましょう。
コメントで補足できるとなお良いですね。
/// <summary>
/// 平均スコアを計算する。
/// 注意:スコアの量によっては値を戻すまでに数分かかる。
/// </summary>
/// <returns>平均スコア</returns>
double ComputeMeanScore()
{
// 実はこの中で平均の計算が走る
// 場合によっては数分かかる
// でも読み手の期待通りの動作ではある
}
要は、「この処理、皆はなんて呼ぶ?」問題です。
処理によってどのような英単語を選ぶかは個人的には結構難しい問題です。慣れましょう。
- first-lastなのか、begin-endなのか(endは配列末尾の1つ先を指すことが多い)
- downloadなのかfetchなのか
- showなのかdisplayなのかoutputなのか
汎用的な命名はできるだけ避ける
イテレータにはi, j, k、一時的に使用する変数にはtmpなど、エンジニアによく利用される汎用的な命名法がいくつかあります。
これらは書く分には楽なのですが、後から見返したときに「tmpって何やねん」となりがちです。
その変数のライフサイクルが本当に限られている(数行などの)場合を除き、汎用的な命名法は避けましょう。
また、スコープが限られていたとしても、イテレータには何かしらの情報を持たせた方が分かりやすいことが多いです。
int[,] matrix = new int[,]
{
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
// 行に対するイテレータをrow,
// 列に対するイテレータをcolで表す
for (int row = 0; row < matrix.GetLength(0); row++)
{
for (int col = 0; col < matrix.GetLength(1); col++)
{
Console.WriteLine(matrix[row, col]);
}
}
変数・定数編
できるだけスコープを縮める
変数のスコープが広いということは、その変数を広いスコープのどこでも変更できる、ということです。
「この変数はどこで変更されるのだろう...」などと気にしながらコーディングするのは大変です。
ということで、変数のスコープはできるだけ縮めましょう。スコープを縮めれば、変数の挙動の理解が容易になりますし、予想外の箇所で変更されないので安全です。
この原則に照らし合わせると、どこでも変更できるグローバル変数を使うなどということはあってはなりません。
...絶対に使ってはいけない、ということもないのですが、何の制約もなくコードのあちこちで変更できるグローバル変数はバグのもとでしかないと思います。
変数を不変にする
変数なのに不変とはこれ如何に。
要は、「スコープを縮める」の別解です。スコープが広かろうと、その変数が不変なら中身は常に一緒なので安心して使えます。
コンパイル前に値が決まっているなら定数にしましょう。
初期化時に値を割り当てて、以降変更できない定数とする方法もあります。C#でいうreadonlyフィールドです。
変数の必要性を検討する
基本的に、不要な変数は無いほうがいいです。
「変数が多い」=「考えなければならないことが多い」=「理解しにくい」です。
例えば、下記のコードはtodayという中間結果を保持するためだけの変数を有しています。
public static void DisplayToday()
{
DateTime today = DateTime.Today;
Console.WriteLine(today.ToString());
}
そこまで悪い書き方でもないと思いますが、1行で書いた方がすっきりします。
public static void DisplayToday()
{
Console.WriteLine(DateTime.Today.ToString());
}
逆に、下記のコードはやや詰め込み過ぎかもしれません。
public static void DisplayToday()
{
// DateTime.Todayは時刻も含む。時刻を省いて表示
Console.WriteLine(DateTime.Today.ToString().Split(' ')[0]);
}
変数を使って処理を分割しましょう。
public static void DisplayToday()
{
// DateTime.Todayは時刻も含む。時刻を省いて表示
string today = DateTime.Today.ToString().Split(' ')[0];
Console.WriteLine(today);
}
変数の必要性の判断は人によると思います。明確な判断基準があるわけではありませんが、「第三者から見てパッと理解できるか?」という問いかけは有効でしょう。
特定の意味がある値を定数にする
要は、マジックナンバー禁止令です。やめましょう。
マジックナンバーとは、「特定の意味を持つが、コード中にベタ書きされている数」のことです。
例として、円周率を使って円の面積を求める下の関数を考えましょう。3.14という数が突然現れています。
「3.14は円周率(の近似値)である」という前提を共有している相手には伝わるでしょうが、不親切なのは間違いありません。
また、「円周率の桁数を増やしたい!」となったときに、円周率をベタ書きしている全ての箇所を直さなければなりません。
public double ComputeAreaOfCircle(double radius)
{
return 3.14 * radius * radius;
}
ちゃんと定数にしましょう。
public const double PI = 3.14; // 実際は標準ライブラリのMath.PIを使うべき
public double ComputeAreaOfCircle(double radius)
{
return PI * radius * radius;
}
enum、構造型、専用のデータ型を使う
変数が何か特別な意味を持つ場合、enumや構造型、あるいは専用のクラスを自分で定義して使うことを検討しましょう。
例えば、レストランのメニューに関する情報として、「料理名」・「値段(円)」・「標準調理時間(分)」を変数にしたいとします。
これを辞書型やリストを駆使して実装するのも悪くはないです。
Dictionary<string, List<int>> menu = new Dictionary<string, List<int>>()
{
{"ramen", new List<int> { 1000, 10 }}
};
しかし、このように書くと以下のような問題が発生します。
- 辞書型のValueに当たるリストを利用したい場合、「最初が値段、その次が標準調理時間」という前提知識が求められる
- そもそも、「リストの要素数が2である」という運用を守らなければならない
代わりに、専用の型を用意してあげましょう。C#では構造体が使えます。
public readonly struct Menu
{
public Menu(string name, int price, int minsToCook)
{
Name = name;
Price = price;
MinsToCook = minsToCook;
}
public string Name { get; }
public int Price { get; }
public int MinsToCook { get; }
}
...
Menu menu = new Menu("ramen", 1000, 10);
コメント編
不要なコメントは書かない/"What"ではなく"Why"を書く
コメントに対する姿勢として、「不要なコメントは書かない」ことが大切です。
コメントを書くということは、メンテナンスすべきドキュメントを増やすということです。該当箇所の処理を変えたときにコメントも変える必要があります。嬉しくはないですね。
単純に行数が増えて見づらくなるのも良くない点です。
不要、というのはかなり主観的な言葉ですが、例えば以下のような「1行の処理が何(What)であるか」を書いたコメントは大多数の人が不要と思うでしょう。なぜなら、大抵のエンジニアが見ればわかることを書いているからです。
// DateTime.Todayの結果を文字列にし、半角スペースで分割し、最初の要素を取得する
string today = DateTime.Today.ToString().Split(' ')[0];
それよりも、「なぜ(Why)この処理をしているのか」を書くほうが有用です。
他のエンジニアがそのコードを読んで内容に疑問を持ったとしても、コメントを見て自己解決してくれることが期待できるからです。自分にとっての備忘録にもなります。
// DateTime.Todayは時刻も含む。時刻を省いて表示
string today = DateTime.Today.ToString().Split(' ')[0];
他にも、下記のようなことはコメントに残しておくべきでしょう。
- コード利用に当たっての注意点
- TODO
- 定数の説明
例外として、「複雑な処理にせざるを得ないとき、その処理内容をコメントする」ことはWhatを書いていますが役立つでしょう。
コードの改善をさぼるためのコメントをしない
前述の内容と少しかぶりますが、コードの書き方で改善できることは改善しましょう。
例えば、分かりにくい変数名を解説するコメントを入れるより、変数名を分かりやすくした方が良いです。
命名編のわかりやすい名前にするにも例を書きました。
コードの塊に要約を付ける
コメントのもう一つの使い方として、「要約を付ける」というものがあります。
C#ではXMLドキュメントコメントのタグの一つにsummaryがあります。まさに要約です。
要約のつける対象は「コードの塊」です。
数行の処理から数百行のクラスまで、必要と思われる箇所にはどんどん要約を入れましょう。
/// <summary>
/// エントリーポイント
/// </summary>
public static void Main(string[] args)
{
// 1から10までカウントアップする
for (int i = 1; i <= 10; i++)
{
Console.WriteLine(i);
}
// 10から1までカウントダウンする
for (int i = 10; i >= 1; i--)
{
Console.WriteLine(i);
}
}
おわりに
(アドベントカレンダー間に合った...)
【処理・関数編】【クラス・ユニットテスト編】を考え中です。いつか書きます。
参考文献
- Dustin Boswell (著), Trevor Foucher (著), 角 征典 (翻訳)、『リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice) 』、2012
- TomLong (著), 秋勇紀 (翻訳), 高田新山 (翻訳), 山本大祐 (監訳) 、『Good Code, Bad Code ~持続可能な開発のためのソフトウェアエンジニア的思考』、2023