はじめに
同僚から勧められてClean Codeを読みました。
全体的にリーダブルコードや単体テストの考え方/使い方と同じことが書かれていましたが、それらよりも、より思想が尖っているという印象を受けました(笑)。
読んだ中で、印象に残ったことをアウトプットのために記事にします。
1章 続編と原則
p41 ボーイスカウトの原則
キャンプ場を、自分が見つけたときよりもきれいにすること
コードを書いていると、"うわ、このコード汚いけど直すの面倒くさいからそのままにしておこう"と思ってしまうことがよくありました。
小学生の時によく言われたうっとうしい格言が大好きなので、自戒も込めてこの言葉を思い出しながらコードを書いていこうと思いました。
チームメンバー全員がこの原則のもとに行動していると、きれいなコードを残し続けることができるかもしれませんね、、、
3章 関数
p64 小さいこと!
尖っているなぁと思ったポイントの一つです(笑)。
関数はできるだけ小さくすることということです。
よく聞くことではありますが、本書では異常なほど小さく切り出していました。
電卓マジックを例に、リーダブルコードとの比較コードを記載します。
(個人的な解釈なので、間違っていたらごめんなさい)
int your_number_three;
int your_number_six;
int step_one, step_two, step_three;
void dentaku_majic() {
// 数字を入力
std::cout << "好きな数を3桁打ち込んでください\n";
std::cin >> your_number_three;
your_number_six = your_number_three * 1000 + your_number_three;
std::cout << "好きな数をもう一度打ち込んで6桁にした数は" << your_number_six << "です" << std::endl;
// 電卓マジックを実行
step_one = your_number_six / 7;
step_two = step_one / 11;
step_three = step_two / 13;
// 結果を出力
std::cout << "7で割ると" << step_one << std::endl;
std::cout << "11で割ると" << step_two << std::endl;
std::cout << "13で割ると" << step_three << std::endl;
std::cout << "最初の数字は" << your_number_three << "でした。" << std::endl;
}
int your_number_three;
int your_number_six;
int step_one, step_two, step_three;
void input_your_number() {
// 数字を入力
std::cout << "好きな数を3桁打ち込んでください\n";
std::cin >> your_number_three;
your_number_six = your_number_three * 1000 + your_number_three;
std::cout << "好きな数をもう一度打ち込んで6桁にした数は" << your_number_six << "です" << std::endl;
}
void process_dentaku_majic() {
// 電卓マジックを実行
step_one = your_number_six / 7;
step_two = step_one / 11;
step_three = step_two / 13;
}
void output_result() {
// 結果を出力
std::cout << "7で割ると" << step_one << std::endl;
std::cout << "11で割ると" << step_two << std::endl;
std::cout << "13で割ると" << step_three << std::endl;
std::cout << "最初の数字は" << your_number_three << "でした。" << std::endl;
}
void dentaku_majic() {
input_your_number();
process_dentaku_majic();
output_result();
}
一つ上の抽象度のブロックを切り出すことで"コードの読み飛ばしを可能にする"という観点は共通しています。
リーダブルコードではそれをコメントで、Clean Codeでは関数を小さく切り出し、その関数名でコードの読み飛ばしを可能にしています。
小さく切り出した関数に適切な形式でコメントを記載すると、VScodeなどでカーソルを合わせた時にその関数の説明が日本語で出るので、英語が苦手な方でも読み飛ばしをすることができると思います。
p71 関数の引数
これも尖っているなぁと思いました(笑)。
関数の引数が多いと理解に時間がかかるため、できるだけ0個または1個に抑えることということです。
本書のコードを読むと確かにその通りなのですが、関数型プログラミングを意識したパイプラインアーキテクチャのような構成だと、どうしても引数が増えてしまったという経験がありました。
本書のどこかで書かれていたように、受け渡し用のデータを構造体で定義する方法しかないと思いますが、何か釈然としない、、、
4章 コメント
適切なコメントの使用方法とは、コードでうまく表現することに失敗したときに、それを補うのに使うこと
これも尖っていて好きです。
読みやすいコード = コメントがたくさん書かれているコード
だと思っている人がたくさんいたので、、、(自分も尖っているのかも)
6章 オブジェクトとデータ構造
p137 データ抽象化
getter、setterを使う理由について記載されていました。
これまで、オブジェクト指向ではgetter,setterを用意することでデータへのアクセスを制限していると思っていました。
しかし、getterとsetter両方が実装されているデータは結局publicと同じでは?という疑問もありました。
そこで、本書ではデータを抽象化された形式で表現するためにgetter,setterを用意する
と記載されていました。
本書の例をそのまま記載させていただきます。
前者の例では乗り物の残りの燃料の量を具体的に表現していますが、後者では百分率によって抽象化されているため、データがどのように格納されているか予想がつきません。
public interface Vehicle {
double getFuelTankCapacityInGalllons();
double getGallonsOfGasoline();
}
public interface Vehicle {
double getPercentFuelRemaining();
}
アクセス制限よりも納得感はあるのですが、下記のような疑問も浮かびました。
- 後者の方法だとreturn以上の実装をしているのでは?
- 抽象化の技術が必要なのでは?
p139 データ/オブジェクトの非対称性
手続き型(データ構造を使用するコード)は、新たな関数を既存のデータ構造に影響を与えずに追加することができます。
オブジェクト指向の場合、逆に既存の関数を変えることなく、新たなクラスを追加することが可能です。
逆もまた成立します。
手続き型だと、新たなデータ構造を追加するには、既存のすべての関数を変えなければならないので、難しくなります。
オブジェクト指向の場合、すべてのクラスを変えなければならないので、新たな関数を追加することは難しくなります。
正直わかるようで、わからない、、、
オブジェクト指向を採用するべきか悩んだことがあったので、今後はこの観点も含めて採用を決めていきたいと思います。
どっかに書いてあったやつ
if文
小さく切り出した関数の中でif文が使われているということは、一つの関数の中で2つのことをしているのでよくないということでした。
void turn_LED(int operation) {
// Raspberry PiからLEDをON/OFFする
if (operation == 0) {
digitalWrite(pin, HIGH);
} else {
digitalWrite(pin, LOW);
}
}
int main() {
int is_LED_ON;
std::cin >> is_LED_ON;
turn_switch(is_LED_ON);
return 0;
}
void turn_on_LED(void) {
// Raspberry PiからLEDをONする
digitalWrite(pin, HIGH);
}
void turn_off_LED(void) {
// Raspberry PiからLEDをOFFする
digitalWrite(pin, LOW);
}
int main() {
int is_LED_ON;
std::cin >> is_LED_ON;
if (is_LED_ON == 0) {
turn_on_LED();
} else {
turn_off_LED();
}
return 0;
}
if文を使う場所が違うだけじゃない?とも思いましたが、
可読性の向上に加え、変更に対しては強いのかなとも思いました。
上記の例でLEDを点滅させる処理を追加する場合、
前者の方法では2つの関数(turn_LED,main)の変更が必要になる一方、
後者の方法では1つの関数(main)の変更と1つの関数の作成になります。
条件のカプセル化
条件判断もカプセル化することで可読性を向上させることができます。
関数名を上手につける必要がありますが、その技術があれば実践するべきかなと思いました。
if (a == 1 && b == 2 && c == 3) {
d = 4;
}
if (should_d_change()) {
d = 4;
}