Clean Code - Chapter 3-2 : 関数
最近、忙しくてだいぶ遅くなってしまいましたね TT...
4. 副作用を起こすな!
ある関数に副作用があるということは、その関数が少なくとも2つの役割を持っているということです。
副作用というのは結局、他の処理も行っていることを意味します。
時には予期せずクラスの変数を修正したり、関数に渡された引数やシステムのグローバル変数を修正したりします。
以下のコードを見てください。
public class UserService {
private static int staticUserId;
public void updateUserEmail(int userId, String newEmail) {
// 機能 1 : データベースでユーザーのメールアドレスを更新
Database db = new Database();
db.updateUserEmail(userId, newEmail);
// 機能 2 (副作用) : グローバル変数の修正
staticUserId = userId;
}
public static void main(String[] args) {
UserService userService = new UserService();
userService.updateUserEmail(1, "newemail@example.com");
}
}
上記のコードでは、updateUserEmail()メソッドがuserEmailを更新する機能を持っています。
しかし、updateUserEmail()の内部を見ると、userEmailを更新するだけでなく、staticUserIdを修正する機能も持っていることがわかります。
関数は一つの役割だけを持つべきであるという原則を守っていないことも問題ですが、果たしてupdateUserEmailという名前を見てstaticUserIdが変更されることを予想できるでしょうか?
4-1 出力引数
一般的に、私たちは引数を関数の入力として解釈します。ある程度プログラミングの経験を積んでいるならば、引数を出力として使用する関数に違和感を感じるでしょう.
以下のコードを見てください。
public static void updateMap(HashMap<String, Integer> map, String test) {
map.put("test", test);
}
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
updateMap(map,test);
System.out.println(map.get("test"));
}
このコードを実行したときの結果を簡単に予測できるでしょうか?もちろん、それほど複雑なコードではないため、予測できるかもしれません。しかし、次のコードと比較してみたらどうでしょうか?
public static HashMap<String, Integer> updateMap(HashMap<String, Integer> map, int value) {
map.put("test", value);
return map;
}
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map = updateMap(map, 10);
System.out.println(map.get("test"));
}
このコードは、mapが確実に変更されることをより直感的に理解できます。
5. エラーコードより例外を使用せよ!
関数が何らかのロジックの実行に失敗したときに、エラーコードを返す方法は可読性と保守性を大きく損ないます。
以下は、現在のプロジェクトで使用されているコードを再現したものです。
問題点が何か見えますか?
HashMap isValidIdAndPwFromUser = outApiService.checkIdAndPwFromUser(totalLoginDTO);
if (isValidIdAndPwFromUser.get("results").equals(false)) {
return ErrObj.error(UserError.NONE_SELECTOR_USER);
}
boolean hasRedisKey = redisService.hasKey(totalLoginDTO.getUserId());
if (!hasRedisKey) {
return ErrObj.error(UserError.NONE_SELECTOR_USER);
}
...
特定のメソッドを呼び出すたびに、応答値の有効性検証を追加で行わなければなりません。
一方で、次のコードを見てください。
try {
outApiService.checkIdAndPwFromUser(totalLoginDTO);
redisService.hasKey(totalLoginDTO.getUserId());
...
} catch (UserNotFoundException e) {
return ErrObj.error(UserError.NONE_SELECTOR_USER);
}
「大きく変わっていないのでは?」と感じるかもしれませんが、有効性をチェックすべきメソッドが増えるほど、その違いは大きくなります。
なぜこのコードを例に示すのかというと、現在のプロジェクトを進める中でこの部分にストレスを感じているからです。
どれだけメソッドを機能別に小さく分割しても、有効性をチェックするロジックのせいでコードが綺麗に見えません。
とはいえ、現在進行中のプロジェクトで自分だけがコードスタイルを変えるのは難しいので、次に新しいプロジェクトを行う際には、この部分を積極的に提案する予定です。
6. 繰り返しを避けよ
重複はコードの長さを増やすだけでなく、アルゴリズムが変わると関連するすべてのコードを修正する必要があります。この過程で一部を修正し忘れると、大きなエラーが発生する可能性が高まります。
重複はソフトウェアにおけるすべての悪の根源です。この重複を排除するために、多くの原則や技法が生まれました。オブジェクト指向では、重複を排除する目的で親クラスにコードを集め、重複をなくすことができます。
結論
「クリーンコード」の本の中で最も重要な章だと思います。
その中でも特に共感した内容は
-
「2.一つのことだけをしろ!」
-
「5.エラーコードに関する内容」 でした。
個人的な経験では、抽象化レベルに応じて関数を分け、適切な名前をつけ、エラーコードの代わりに例外を使用することで、本当にクリーンなコードが書けるようになると思います。
まだ経験が1年しかないので、「お前が何を知っているのか?」と言われるかもしれませんが、周りの優秀な開発者の方々も同意してくださったので……許していただければと思います。
ちなみに、コードの例としてMapをよく使用しましたが、DTOを使用する方が良いです!