動作が変わらないようにコードを修正し、計算速度や読みやすさを向上させることをリファクタリングといいます。
リファクタリングは軽視されがちですが、プログラミングにおいて読みやすさは重要です。
誰か他の人が見るときに最もかける時間はコードを読む時間であり、完璧な理解をせずにコード編集するのはとてもリスキーです。
さらに自分が読むときにも、読みやすいコードは重要です。
昔書いたコードはもはや他人の書いたコードと変わりません。読みやすいコードはドキュメント代わりにもなり、自分を助けることにもなるのです。
ここでは読みやすいコードの法則について、初心者でも理解できる重要なものを説明していきます。
変数は不変
変数なのに不変とはいかに?と感じるかも知れませんが、変数の中の値は変わっても問題ありません。
重要なのは、変数の中身の意味が変わらないプログラムです。
意味のある変数名を使うことが一番の基本です。適した名前をつけてあげることで、無理して覚える変数が少なくなります。
読む人の脳のリソースを使わないことで、アルゴリズムを読み解く事に集中できるようになります。
例えば下記のようにtmpの中身をコロコロ変えると、動作が理解しにくいコードになりやすいです。
float tmp = a * b;
tmp += c;
if(tmp < d) { tmp = d; }
引数も変数にしない
関数の引数も、値渡しなら基本的には変更しない方が良いです。
こうすることで、ポインタで渡す場合は中身の変更であり、値渡しの場合は返り値の計算のための入力値となることが明確になります。
もし値渡しで引数とした変数の中身を変更したい場合は、新しい変数を用意して役割の分かる名前を付けるようにします。
例えば下記では、関数内で引数を途中で変更しているため、引数の意味が理解しにくくなります。
void add(int x){
x++;
cout << x << endl;
}
こういった場合は多少冗長でも良いので、別途変数を用意してあげるほうがわかりやすいです。
引数を減らす
引数をint、floatなどのプリミティブ型に固執すると可読性が落ちます。
引数はクラスにしてまとめて渡してあげると非常にわかりやすいコードが書けます。
さらに大量の変数を渡す必要がなくなり、コードの重複やミスも防げます。
初心者が作ったプログラムでは、下記の様に可読性の低いものをよく見ると思います。
void calcPrice(int num, int amount, float price, float tax, float discount){
}
クラスの勉強は負荷の高いものですが、時間をかけるだけの価値は十分にあります。
戻り値もintやfloat型でなくクラス型にすることで、関数の機能を理解しやすくなります。
引数で重要なことについてまとめると、下記のとおりです。
- 引数を関数内で修正しない
- 出力として引数を使わない
- 引数は少なくする
ifのネストを減らす
ネストが深すぎると、コードのロジックが理解しにくくなります。
読む人にとってネストが深い部分は、全てのif文の条件を考えながら並行してロジックを読むことになります。そのため、脳のリソースを非常に使う部分となります。
そんなときは、ロジックをまとめて関数化し、returnで終わらせることでロジックがシンプルになります。
条件が多い場合も、else ifを多用するとコードが見にくくなりますが、returnを使うことで解決します。
下記のようにifのネストが続くと、コードのロジックが理解しにくくなります。
if(A){
if(B){
}elseif(C){
}
}
一方で、下記のようにreturnで返せると、一つのif文しか覚えなくて良いので非常に楽です。
if(A) return;
if(B){
return;
}else if(C){
}
returnで返せる条件は多くはありませんが、少し変えるだけでも可視性がぐっと上がります。重要なテクニックとして覚えておきましょう。
クラスメソッドの基本
クラスの機能に関する説明はよく聞きますが、推奨の使い方を聞く機会は少ないです。
ここではクラスメソッドの重要なポイントについて軽く説明します。
メソッドは自身の変数を使う
クラスメソッドでは、必ず自身の変数を使うようにします。さらに言えば、他のクラスの変数をほとんど使わないほうが好ましいです。
クラスとは一つの物体(オブジェクト)として考える枠組みなので、クラス間で変数を渡し合うと意味が理解しにくくなります。
実際に存在するオブジェクトとして考えにくいなら、例えば初期化などの抽象的な概念をクラスにすることも可能です。
例えばゲッターやセッターはクラスの機能説明でよく使われますが、実際に使うと非常に読みにくいコードになります。
ゲッターもセッターも他のクラスや関数からクラス変数を変更するためのものです。
そもそもクラス外からいじること自体がロジック理解を妨げるので、ゲッターとセッターは避けるのが吉です。
カプセル化はオブジェクト指向の重要な要素ですが、「カプセル化=ゲッター、セッター」ではありません。
「カプセル化 = データとその操作の関数をまとめて、メソッドのみ公開すること」と覚えましょう。
条件分岐はクラスメソッドにまとめる
switchなどで条件分岐を行う場合、項目が増えると修正する必要があります。ただしswitch文がちらばってると、修正漏れのリスクが高まります。
そのため、条件分岐はクラス内の関数としてまとめてしまうことで、全体像が見えやすく修正が容易になります。
class A(){
Amethod(int type){
switch(type){
case 1:
case 2:
}
}
}
もし何か不自然な挙動をした場合は関数ごと扱えるため、デバッグも簡単になります。
継承
継承はクラスの機能説明でよく出てくる上に関連機能が多いので、つい使えそうなときは使いたくなりがちです。
しかし、継承を多用すると読みにくいコードになってしまうので、非常に危険であり、非推奨です。
例えば継承後にオーバーライドすると、どのタイミングで関数が入れ替わったのかを時系列で追わないといけません。
そのため、継承を使うと動作を追いかける作業が複雑化し、理解が難しくバグの温床になります。
そこで継承の代わりに、下記のように別の関数を作って明示的に分けて考えるようにすると良いです。
class Fighter(){
private Human human;
int attack(){
return human.attack() + 10;
}
}
上記では継承せずに、もとのクラスの値を参考にして値を決めることが出来ています。
これにより、継承よりも元関数の不具合の影響を受けにくくなります。
上記のように書けば、もしFighterクラス内でattack()を使いたい場合に、fighter.attack()とhuman.attack()ののどっちを使うのかを明示的に選択でき、混乱がなくなります。
おわりに
読みやすいコードを書くためのリファクタリングのコツについて解説しました。
今回は、変数は変更しない、関数の引数を減らす、ネストを減らす、クラスメソッドの使い方、継承について紹介しましたが、これらは基本的な事項であり、リファクタリングの方法はまだまだあります。
例えば、超基本的な下記のようなテクニックもありますし
- 変数の名前はわかりやすくする
- 否定文を減らす
- ロジック(条件)をメソッド(関数)に変える
他にも高度なテクニックは様々です。
ただ、「読みやすい」というのは人それぞれであるため、リファクタリングの書籍の全てを真似できれば読みやすくなるかというと、実はそうとも言えません。
結局は開発者の仲間が読みやすいように最適化してあげるほうが価値があります。
今回紹介したものは、数あるリファクタリング技術の中でも人によらず一般的に使いやすいものばかりです。
ぜひ安心してご使用ください。
あとはたくさんコードを書いて読みやすいプログラムを作れる技術者になってください。
参考:良いコード/悪いコードで学ぶ設計入門