Clean Code アジャイルソフトウェア達人の技の本を整理した内容です
第3章 関数
どんなプログラムでも、最も基本的な単位は関数です。この章では、関数をうまく作成する方法を紹介します。
1. 小さく作りましょう!
関数を作るための第一のルールは「小さくする!」です。関数を作るための第二のルールは「さらに小さくする!」です。
では、どれくらい短くすれば良いのでしょうか?
この本の著者は、インデントレベルが2段を超えてはいけないと言っています。そうすることで、関数は読みやすく理解しやすくなります。
2. 一つのことだけを行いましょう
関数は一つのことを行うべきです。その一つのことをうまく行うべきです。
なんだかウォーレン・バフェットのアドバイスを思い出させる文ですね
これは過去40年にわたり、様々な表現でプログラマーに与えられてきた助言です。
この助言の問題点は、「一つのこと」が何なのかを知るのが難しいという点です。
以下のコードで、calculateAndPrint()関数は3つのことをしているように見えるのではないでしょうか?
public void calculateAndPrint(int a, int b) {
int sum = calculateSum(a, b);
printSum(sum);
printEvenOrOdd(sum);
}
private int calculateSum(int a, int b) {
return a + b;
}
private void printSum(int sum) {
System.out.println("合計: " + sum);
}
private void printEvenOrOdd(int number) {
if (number % 2 == 0) {
System.out.println("偶数です.");
} else {
System.out.println("奇数です.");
}
}
見たところでは、calculateAndPrint()
関数が3つの仕事をしているように見えるかもしれませんが、ここで重要なのは各関数の抽象化レベルです。
calculateAndPrint()
関数は「計算結果を出力する」という一つの高レベルの作業を行っており、この作業を行うために必要な詳細な作業は別の関数に分離されています。
calculateSum()
、printSum()
、printEvenOrOdd()
関数はそれぞれ一つの仕事だけを行っており、calculateAndPrint()
よりも一段階低い抽象化レベルを持っていると考えられます。
calculateAndPrint()
関数はこれらを組み合わせて、より抽象化された作業を行う役割を果たしています。
したがって、この場合、calculateAndPrint()
関数は「一つのこと」だけをうまく行っていると言えるでしょう。
これは、関数が行う作業の抽象化レベルを理解することが重要であることを示しています。関数が何をしているかを判断する際、その作業が高レベルの「一つのこと」なのか、それともcalculateAndPrint()
の中で直接計算を行って複数のことをしているのかによって、関数の品質が変わってくるのです。
もし、関数の中で意味のある名前で他の関数を抽出できるなら、その関数は複数の作業を行っているということです。
2-1 降りる規則
コードは上から下へと小説のように読めるのが理想です。ある関数の後には、抽象化レベルが一段階低い関数が続くべきです。つまり、上から下へコードを読むと、関数の抽象化レベルが一度に一段階ずつ下がることを意味します。著者はこれを降りる規則と呼んでいます。
テーマが難しくても、内容がよく構成されている本は理解しやすく、さらに多くのことを知りたくて次の巻を買いたくなるほど引き込まれます。しかし、テーマが難しく理解しづらい構成であれば、誰もその本を読みたがらないでしょう。
関数も同じです。難しく書けば書くほど抽象化のレベルが曖昧になり、それに伴って理解が難しくなります。
2-2 関数の名前
良い名前がもたらす価値は、いくら強調してもしすぎることはありません。コードを読んで想像した機能がそのまま実行されるのであれば、それをクリーンコードと呼んでも良いでしょう。特に、一つのことだけを行う小さな関数に理解しやすい名前が付いていれば、それは最高です。要約すると、以下の通りです。
- 長くて説明的な関数名は、短くて難解なものよりも良い。
- 長くて説明的な関数名は、説明的なコメントよりも良い。
- 関数名を決める際には、複数の単語が読みやすい命名法を使用する。
- 複数の単語を使って関数の機能を適切に表現する名前を選ぶ。
- 関数名は一貫性があり、モジュール内で同じフレーズ、名詞、動詞を使用すること。
- 関数の意図や引数の順序と意図を正しく表現するべきです。
→ 例えば、関数と引数が動詞/名詞のペアを成すようにする。
→ そして、関数名にキーワード(引数名)を追加すると理解しやすくなる。
3. 関数の引数
関数で最も理想的な引数の数は無引数です。単引数と二引数までは許容範囲ですが、三引数は可能な限り避けるべきであり、4つ(多引数)の場合は特別な理由が必要で、できるだけ使用すべきではありません。
引数はコードを書いた人ではなく、見る人の立場から非常に理解しにくいことがあります。毎回引数がどのように変わるのかを把握するだけでなく、最終値を導き出す際に引数がどこで止まり、どの変数に格納されて返されるのかを確認しなければならないからです。
一つだけでも分析が難しいのに、引数が複数ある場合は当然さらに難しいでしょう。したがって、避けられない場合は、引数の数を最大限単引数にすることを目標にし、可能であれば無引数にする練習を多く行う必要があります。
関数の引数について、さらに詳しく見てみましょう。
3.1 フラグ引数
フラグ引数は本当に厄介です。関数の引数にboolean値を渡すということは、その関数が二つ以上のことを行うことを明示しているからです。
なぜか? boolean goAimyonConcert
が引数として渡された場合、goAimyonConcert
の値が真であればこれを行い、偽であればそれを行うという意味になるからです!
以下のように、関数をあらかじめ作っておき、goAimyonConcert
の値に応じて関数を呼び出す方が良いでしょう。
public void attendConcert() {
...
}
public void stayHome() {
...
}
3.2 引数オブジェクト
もし引数の数を減らせない場合は、クラス変数に渡すことも良い方法です。
public void reservateAimyonConcert(double price, int totalTickets, Date concertStartDate) {
...
}
上記のコードを以下のように変更できます。
@AllArgsConstructor
@Getter
public class ConcertReservation {
private double price;
private int totalTickets;
private Date concertStartDate;
}
public void reserveAimyonConcert(ConcertReservation reservation) {
double price = reservation.getPrice();
int totalTickets = reservation.getTotalTickets();
Date concertStartDate = reservation.getConcertStartDate();
...
}
3.3 動詞とキーワード
関数の意図や引数の順序と意図を正しく表現するためには、良い関数名が不可欠です。
例えば、devide()よりもdevideFirstParamBySecondParam()の方が、どちらに対して割り算を行うべきかが明確にわかります。
devide(a, b);
devideFirstParambySecondParam(a, b);
「まだ書いていない第3章の部分は、引き続き執筆する予定です。」