読みやすさや見やすさを意識してコードを書くプログラマは意外なほど少ないように思います。しかし熟練のプログラマは読みやすいコードを書く傾向にあります。一流のプログラマはコードに規則性を見出すものなのです。
規則性の高いコードは可読性や判読性を高める効果があり、コードの理解のしやすさを向上させるだけでなく、読み手の負担を軽減させる効果もあります。
// 一般的なコード
int leftMargin = 32;
int rightMargin = 64;
// より良いコード
int marginLeft = 32;
int marginRight = 64;
一般的なプログラミングでは、コードを書く時間よりも読む時間のほうが長くなるものです。それゆえコードの可読性は非常に重要なテーマとなっています。
本記事では、規則性が高く、読みやすく、読み手にとって理解のしやすいコードを書くためのコーディング テクニックを紹介していきます。
目次
- 変数の代入処理を揃える
- コメントを揃える
- 変数名や型名を揃える
- 条件式を揃える
- 関数定義を揃える
- 横方向を活かす
- その他の例
- 変数名を揃える
- 命名規則を工夫する
- 略称で揃える
- 意図的に変数を括弧で囲う
- 意図的に添字アクセスする
- 意図的にエスケープ文字を使用する
- 意図的に符号を用いる
- 不格好でも条件とelse文の条件を揃えてみる
- switch文を利用する
- 実引数をヨーダ記法風に記述しない
- 関数呼び出し式を揃える
- コード整列の哲学
- コード整列の原則
- コード整列を行うべきケースと行うべきでないケース
コード整列/コード揃え/コードアライメント
垂直方向を意識してコードの位置をそろえます。
int left = 32;
int right = 64;
switch (align) {
case Left: return "Left";
case Right: return "Right";
}
int getValue() {}
void setValue() {}
=
演算子による代入処理とreturn
文による返却処理などが綺麗に揃えられている所が本プログラミング作法の特徴です。
これによってコード間の関連性や連続性を表現することができます。より具体的な効果とメリットについては次から説明していきます。
変数の代入処理を揃える
int left = 32;
int right = 64;
上記のコードは文法的には全く問題のないコードですが、読みやすいコードとはいえません。
left
とright
(左と右)は対になる変数ですから、両者の代入演算子の位置を揃え、より両者の関係性を明確に示す必要があります。
int left = 32;
int right = 64;
次のような、系統の異なる変数グループと混在しても、両グループがコード整列によって明確に区別されるようになるというメリットも得られます。
int left = 32;
int right = 64;
int leftPadding = 8;
int rightPadding = 4;
left/botomのグループとleftPadding/rightPaddingのグループは異なる位置でコードが揃えられており、それが両グループを区別する際の明確な指標となる。
本来は以下のように空白で両グループを区別していましたが、上記のようなコード整列を行えば以下のような区別も不要になります。
int left = 32;
int right = 64;
int leftPadding = 8;
int rightPadding = 4;
もっとも両グループの揃え位置が同じになってしまうような場合には、従来どおり空白による区分けが必要になります。
int margin = 8;
int padding = 4;
int z_index = 4;
ここまでのまとめ
- 代入演算子を揃えることで複数ある変数の関係性を表現できる
- コード整列によって変数のグループ化が実現できる
- グループ化によって揃えられた代入位置を元に、グループ間の区別が可能になる
コメントを揃える
ドキュメント的/仕様書的な読みやすさを意識してコメントを記述するようにします。
enum Alignment {
Left, // 左揃え
Right, // 右揃え
Center, // 中央揃え
}
conv("0b10"); // 2 ( 2進数変換)
conv("0x0F"); // 15 (16進数変換)
conv("011"); // 9 ( 8進数変換)
conv("0101"); // 65 ( 8進数変換)
conv(" 011"); // 9 ( 8進数変換)
conv( "011"); // 9 ( 8進数変換)
プログラムのコードは表計算ソフト(Excel等)の表を意識して書くようにすると読みやすいものになります。
ここまでのまとめ
- プログラムのコードは表を意識して書くようにする
変数名や型名を揃える
char currentToken;
int currentIndex;
private String firstName;
public String lastName;
少し奇抜ではありますが、次のような揃え方も考えられます。
private String firstName;
public String lastName;
条件式を揃える
似たようなパターンの条件式が複数列挙されているような場合には、両条件式を揃えることができます。各条件の改行タイミングはオペランドを優先とし、常にオペランドが先頭に位置するように改行します。
// Good
if (c == '\n' || c == '\r' ||
c == '\t' || c == '\v') {
return true;
}
// NG
if (c == '\n' || c == '\r'
|| c == '\t' || c == '\v') {
return true;
}
先頭にオペレータを配置したい場合には、ダミーの式で文脈を調整するテクニックが活用できます。しかし一般的なテクニックではありません
// SQL-er
if (1 == 1
&& c == '\n' || c == '\r'
|| c == '\t' || c == '\v') {
return true;
}
ただ、SQLの世界では比較的有名な技法として使われています。もっともSQLの場合は条件式を動的に追加する用途で活躍するテクニックだという違いがあります。
query("select * from t where 1 = 1"
+ (flag1 ? " and a = 1" : "")
+ (flag2 ? " and b > 2" : "")
);
常識にとらわれずに、以下のようなコードを書くこともオススメです。
if ( c == '\n' || c == '\r'
|| c == '\t' || c == '\v') {
return true;
}
関数定義を揃える
数値演算の箇所が揃っている点が重要です。
fn titleFontSize() -> int { Font.systemFontSize() + 4 }
fn captionFontSize() -> int { Font.systemFontSize() - 1 }
定義方法を工夫してコードを揃えるテクニックです。
// Before
iterator begin() { return v.begin(); }
const_iterator cbegin() const { return v.cbegin(); }
// After
auto begin() { return v.begin(); }
auto cbegin() const { return v.cbegin(); }
閉じ括弧を揃えるようにすると、まとまりを感じさせるコードになります。
auto begin() { return v.begin(); }
auto end() { return v.end(); }
auto cbegin() const { return v.cbegin(); }
auto cend() const { return v.cend(); }
空間や奥行きを感じさせるようなコードを意識してみましょう。
横方向を活かす
// Before
template<class T>
auto length(const T* s) {
return strlen(s);
}
template<class T>
auto length(const T& s) {
return s.size();
}
// After
template<class T> auto length(const T* s) { return strlen(s); }
template<class T> auto length(const T& s) { return s.size(); }
コードをコンパクトにまとめる意図があります。両コードの違いを識別しやすくなります。また空行を省略しても違和感のないコードになります。縦方向に長いコードを避け、横方向を活かしたコードを書くよう意識しましょう。
その他の例
開始括弧や終わり括弧、Key-Value要素を揃えると読みやすいコードになります。
// 開始括弧を揃える
int foo() { return !(1 & f); }
int bar() const { return (1 & b); }
// keyとvalueを揃える
html .css {
margin-top: 48px;
margin-right: 32px;
margin-left: 32px;
margin-bottom: 48px;
}
// 桁や`*/`を揃える
var obj = {
left: 128, /* left */
center: 0, /* center */
}
// 閉じ括弧`);`を揃える
assert( contains(String("abc") ,"b") );
assert( contains(String("ABC"), "BC") );
// 条件演算子を揃える
$c = empty($obj['count']) ? '0' : '1';
$l = empty($obj['length']) ? '0' : '1';
$s = empty($obj['size']) ? '0' : '1';
// 名前付き引数のラベル名を右に揃える
[self joinArray:@[@"abc", @"def"]
separator:"\t"
terminator:"\n"];
// 仮引数を揃える
func join(items: [String],
separator: String,
terminator: String) -> String {}
// 実引数を揃える(ラベル名と値を揃える)
self.join(items: ["abc", "def"],
separator: "\t",
terminator: "\n")
// 実引数を揃える(特定の関連した要素群のみ揃える)
self.join(items: ["abc", "def"],
separator: "\t",
terminator: "\n"
修正履歴(2020-01-24): 編集(名前付き引数のラベル名を揃える → 名前付き引数のラベル名を右に揃える)、追加(条件演算子を揃える)
変数名を揃える
変数名を揃えておくと、後続のコードが揃いやすくなるという特徴があります。手動による面倒な整列も不要になります。
// Before
var front = list[0];
var back = list[list.count - 1];
if (front != foo) return 0;
if (back != bar) return 0;
// After
var head = list[0];
var last = list[list.count - 1];
if (head != foo) return 0;
if (last != bar) return 0;
アンダースコアを用いて無理やり文字数を揃えるテクニックもあります。後続のコードでは、手動によるコード整列が不要になるという利点が得られます。
var keydown = Event(KeyDown);
var keyup__ = Event(KeyUp);
EventSetFlags(keydown, flags | keydown.flags);
EventSetFlags(keyup__, flags | keyup__.flags);
EventPost(keydown), Release(keydown);
EventPost(keyup__), Release(keyup__);
もっとも、アンダースコアで終わる変数をプライベートメンバとして扱うコーディング規約もあるため、採用には注意が必要です。またC/C++言語では連続するアンダースコアの利用が言語仕様上、制限されています。
命名規則を工夫する
変数名のグループやカテゴリーをより強調するために、単語の命名規則を工夫します。
// Before
int leftPadding = 8;
int rightPadding = 4;
Color redColor = Color.red;
Color blueColor = Color.blue;
var l = leftFold(foo, bar);
var r = rightFold(hoge, piyo);
// After
int paddingLeft = 8;
int paddingRight = 4;
Color colorRed = Color.red;
Color colorBlue = Color.blue;
var l = foldLeft(foo, bar);
var r = foldRight(hoge, piyo);
垂直方向におけるコードの共通箇所が増えました。
若干「ハンガリアン記法」風になってしまったり、また英文法らしさが損なわれるという問題もありますが、変数のバリエーションを把握しやすくなるという最大の利点が得られます。
本命名規則の利点については、以下の記事でより深い考察を行っています。
なお、コメントや日本語文章の場合には次のような工夫を行うとよいでしょう。
// Before
remove_filter('title' , 'tz'); // タイトルの自動変換を無効化する
remove_filter('content', 'tz'); // 本文の自動変換を無効化する
// After
remove_filter('title' , 'tz'); // 自動変換無効化: タイトル
remove_filter('content', 'tz'); // 自動変換無効化: 本文
remove_filter('title' , 'tz'); // 自動変換無効化 (タイトル)
remove_filter('content', 'tz'); // 自動変換無効化 (本文)
このように文章としての自然さよりも、規則性を重視するようにします。簡素な文は、一般的な文章よりも理解のし易いものになる場合もあります。
略称で揃える
変数名の名称を工夫してコードを揃えるという発想もあります。空白を用いずにコードを揃えることが可能になります。
// Before
int paddingLeft = 8;
int paddingRight = 4;
// After
int paddingL = 8;
int paddingR = 4;
このようにleft/rightを一般的なL/R形式で表現します。
他にも次のような工夫が考えられます。
// Before
struct { int location, length; } range;
range.location = 2;
range.length = 4;
// After
struct { int loc, len; } range;
range.loc = 2;
range.len = 4;
// Before
[1, 2].front
[1, 2].back
// After
[1, 2].head
[1, 2].last
一文字変数の利用も効果的です。
// Before
int left = margin.left;
int right = margin.right;
int top = margin.top;
int bottom = margin.bottom;
// After
int l = margin.left;
int r = margin.right;
int t = margin.top;
int b = margin.bottom;
やりすぎると逆に理解がしづらく解読が困難な名前を生んでしまう可能性があるため注意が必要です。
意図的に変数を括弧で囲う
c
を(c)
と記述することで、①の行と②の行の変数の位置を揃えることができます。
// Before
a = 0xFF & (c >> 8); // ①
b = 0xFF & c; // ②
// After
a = 0xFF & (c >> 8); // ①
b = 0xFF & (c); // ②
a = 0xFF & (c >> 8); // ①
b = 0xFF & (c ); // ③
各行の共通部分が出来るだけ多くなるような書き方を意識するとよいでしょう。
ヨーダ記法の活用
余談ですが、今回は0xFF & ...
という形で、定数を左辺に記述する「ヨーダ記法」を意図的に取り入れています。ヨーダ記法を取り入れないと以下のような見た目になります。
// NG: 【規則性が読み取りづらい】
a = (c >> 8) & 0xFF;
b = c & 0xFF;
// NG: 【歯抜けになり不格好】
a = (c >> 8) & 0xFF;
b = (c) & 0xFF; // インデントの手間も多い
// Good:【ヨーダ記法】
a = 0xFF & (c >> 8);
b = 0xFF & (c);
番外編
コード整列とは直接的には関係はないのですが、可読性を高めるために意図的に括弧を活用するテクニックも紹介しておきます。
sum((3), 1, 2, 3); // C: int sum(int count, ...)
any((s), t, u); // C++: bool any(T target, A... args)
括弧を用いた実引数は他の実引数とは性質の異なる引数であることを示すことができます。
他にも、1 == 1
や1 << 0
のような無意味な記述をあえて使うという発想もあります。他のコードとの一貫性を保つ意図や規則性を維持する効果があります。
enum mask {
a = 1; // NG
a = 1 << 0; // Good!
b = 1 << 1;
c = 1 << 2;
}
a = 1;
のような他とは異なる記述は、読み手の注意を過剰に引いてしまう恐れがあります。a = 1 << 0;
という形で他の記述と合わせることで、これを回避します。もっとも列挙定数が1から始まるということを明示したいような場合にはこの限りではありません。
意図的に添字アクセスする
先頭アドレスへの間接参照をあえて用いないようにします。
// Before
char front() { return *cstr; }
char back() { return cstr[_len - 1]; }
// After
char front() { return cstr[0]; }
char back() { return cstr[_len - 1]; }
意図的にエスケープ文字を使用する
// Before
case '\'': return "'";
case '"': return "\"";
// After
case '\'': return "\'";
case '\"': return "\"";
意図的に符号を用いる
// Before
addkeyDown(KeyUp , ^{ player.addVolumeOffset( 1); });
addkeyDown(KeyDown, ^{ player.addVolumeOffset(-1); });
// After
addkeyDown(KeyUp , ^{ player.addVolumeOffset(+1); });
addkeyDown(KeyDown, ^{ player.addVolumeOffset(-1); });
return c == 1 ? +1 : -1;
マイナスの値とプラスの値が混在するような処理では、プラスの値を強調するために+
による符号を用いるようにします。こうすると一般的な用途で利用されている数値の1
との区別が可能となります。+1
を用いることでマジックナンバーとしての性質が抑えられ、より意味のある値として表現することが可能になります。また同等の数値-1
との関連性を示唆することが可能となります。
不格好でも条件とelse文の条件を揃えてみる
複数の条件式が似たような処理になる場合は、できるだけの条件式とelse文の条件式が綺麗に並ぶように揃えてみましょう。規則性が生まれるため直感的で理解のしやすいコードになります。
if (equals(enc, "UTF8")) {
return 4;
} else if (equals(enc, "EUC")) {
return 3;
} else if (equals(enc, "ASCII")) {
return 1;
}
// if (/* */ equals(enc, "UTF8")) {
// } else if (equals(enc, "EUC")) {
switch文を利用する
上記のサンプルは不自然な記法であり、あまり気持ちのよいものではありません。switch文に置き換えたほうがよりシンプルになり、また規則性を見出しやすくなります。
switch (encoding) {
case "UTF8": return 4;
case "EUC": return 3;
case "ASCII": return 1;
}
また、switch文を記述する際には上記のようにreturn
文を揃えるようにすると、より直感的なコードになり、読みやすさや保守性も向上します。:
直後の改行もあえて省略しワンラインで記述するようにします。
実引数をヨーダ記法風に記述しない
関数に実引数を渡す際に、変数を前方に配置するようにし、定数は後方に配置するのもオススメです。余計なコード整列("EUC" ,
)が不要になります。
// Before
} else if (equals("EUC" , encoding)) {
} else if (equals("ASCII", encoding)) {
// After
} else if (equals(encoding, "EUC")) {
} else if (equals(encoding, "ASCII")) {
関数呼び出し式を揃える
関数呼び出しが入れ子になる場合は、外側の関数の実引数に空白のパディングを挿入するようにします(f(g())
→ f( g() )
)。連続する括弧())
)を排除し、処理の内包関係を把握しやすくする意図があります。
ただ、この手のスタイルはあまり好まれておらず、メジャーなスタイルとは言えませんが、逆にassert関数やprint関数, ログ関数などの特殊な関数で利用するようにすると良いです。
print( contains(String("abc") ,"b") );
print( contains(String("ABC"), "BC") );
またassert文の利用時には、ヨーダ記法による比較を行うことをオススメします。assert文は結論ありきの機能であるため、結果がどうなるのかを把握しやすくするために、このヨーダ記法を活用します。
assert( "ab" == String("abc")[:2] );
assert( "BC" == String("abc")[1:].toUpper() );
ヨーダ記法を使わないケースの場合、診断対象の処理が長く複雑になると、結論(以下の例では文字列定数)の部分が式中の右側に押しやられてしまい、余計なコード整列が必要になってしまいます。
// 読みづらい
assert( String("abc")[:2] == "ab" );
assert( String("abc")[1:].toUpper() == "BC" );
// 余計なコード整形が必要になる場合も
assert( String("abc")[:2] == "ab" );
assert( String("abc")[1:].toUpper() == "BC" );
コード整列の哲学
以下は関連する処理やプロパティのみを揃えるという考え方です。グルーピングの実現が可能になります。
TextField label = new TextField()
label.text = "Title"
label.alternateText = "TITLE"
label.toolTip = "タイトル"
label.drawsBackground = false
label.bordered = false
label.editable = false
label.selectable = false
このように=
記号の開始位置が、要素のまとまりを区別する際の指標となります。
従来のプログラミング作法では空行でコードを区分け/組分けしなければなりませんでした。
TextField label = new TextField()
label.text = "Title"
label.alternateText = "TITLE"
label.toolTip = "タイトル"
label.drawsBackground = false
label.bordered = false
label.editable = false
label.selectable = false
しかし、先程のコード整列を行う方法でもこのような区別を行った際と同等の効力が得られます。
空行との混在について
コード整列を行った上でさらに空行を用いることも有効ですが、その辺はコード分量とのバランスや処理内容との関係もあるため、どちらのスタイルが優れているかは一概には言えません。
TextField label = new TextField()
label.text = "string"
label.toolTip = "string"
label.editable = false
label.selectable = false
この手のコードであれば、リテラル値("string"
, false)がコードの構造を把握する際の強烈な指標となるため、わざわざ空行を用いる必要はないと言えそうです。
TextField label = new TextField()
label.text = "string"
label.toolTip = "string"
label.editable = false
label.selectable = false
なお空行の代わりに、コメントを用いるというアイディアもあります。
TextField label = new TextField()
label.text = "Title" /* 表示内容の設定 */
label.alternateText = "TITLE"
label.toolTip = "タイトル"
label.drawsBackground = false /* 描画方法の設定 */
label.bordered = false
label.editable = false
label.selectable = false
コード整列の原則
関連処理や共通処理に対するコード整列を行う際には、両処理の共通部分が最大限に揃うように調整します。
// 元のコード
keys = []
values = []
// 良い
keys = []
values = []
// より良い
keys = []
values = []
このように値の部分([]
)だけでなく、演算子(=
)まで揃えるようにするとより見やすいコードになります。
カンマ記号の場合も同様です。
// 良い
keys = ["cc", "rb", "py", "php"];
values = ["C++", "Ruby", "Python", "PHP"];
// より良い
keys = ["cc" , "rb" , "py" , "php"];
values = ["C++", "Ruby", "Python", "PHP"];
少し抵抗があるかもしれませんが、次のようなケースでも、ドット演算子を基準とした整列を行うようにすると良いでしょう。
// 元のコード
[keys.allKeys componentsJoinedByString:@","]
[values.allKeys componentsJoinedByString:@","]
// 良い
[keys.allKeys componentsJoinedByString:@","]
[values.allKeys componentsJoinedByString:@","]
// より良い
[keys .allKeys componentsJoinedByString:@","]
[values.allKeys componentsJoinedByString:@","]
連想配列の場合は、言語側の制約にしたがって:
記号による整列を行うようにしましょう。
{ // 良い
cpp: "C++"
java: "Java"
}
{ // 良くない場合がある
cpp : "C++"
java: "Java"
}
{ // 良い
"cpp" : "C++"
"java": "Java"
}
コード整列を行うべきケースと行うべきでないケース
コード整列は比較的手間のかかる作業でもあります。そのため、頻繁に保守することのない書き捨てのコードに対してコード整列を行うことのメリットはそれほど高くはない言えそうです。開発期間が短く、永続的な運用が期待できないようなプロダクトやプロトタイプ開発でも同じです。苦労の割に得られるものが少なくなるような場合もあるため、注意が必要です。
ただ、単体テストや結合テストの期間が長くなるケースであれば、すぐにでもコード整列の恩恵は得られます。また保守期間の長いプログラムや機能拡張を前提としたプロダクト、複数のプログラマが関わるプロジェクトや入れ替わりの激しいプロジェクトではコード整列の恩恵を受けやすい傾向にあります。
自身の開発用途や規模に応じてコード整列を活用するようにしましょう。