前回の記事ではリーダブルコードの中心となる「読みやすいコード」についてと、読みやすいコードがもたらす価値とはどういうものなのか?について解説を行いました。
tech-kodawari-japan.hatenablog.com
今回からリーダブルコードの2章以降に記載されている「読みやすいコード」を書くための方法について触れていきたいと思います。
内容紹介
2章 名前に情報を詰め込む
- 2章 名前に情報を詰め込む
- 2.1 明確な単語を選ぶ
- 2.2 tmpやretvalなどの汎用的な名前を避ける
- 2.3 抽象的な名前よりも具体的な名前を使う
- 2.4 名前に情報を追加する
- 2.5 名前の長さを決める
- 2.6 名前のフォーマットで情報を伝える
- 2.7 まとめ
2章では名前を付けるときのテクニックを解説しています。
コードを書く際は、変数、メソッド、クラスなどの命名と向き合うことになります。そんなとき、道を示してくれる光となる内容です。
本記事では「2.1 明確な単語を選ぶ」「2.3 抽象的な名前よりも具体的な名前を使う」「2.4 名前に情報を追加する」「2.6 名前のフォーマットで情報を伝える」を取り上げて、もう少し分かりやすくなるようにブレイクダウンします!
2.1 明確な単語を選ぶ
本記事ではこの言葉を以下のようにブレイクダウンしました。
「まず修飾語を付ける。そのあとにより具体的な表現を探す」
「まず修飾語を付ける」
例として、船に詰める荷物の量を記録したい場合を考えます。限度を表す単語は「capacity」です。これを使ってクラスを作成するとこうなります。
public class ship { long capacity; }
しかし、これは「capacity」が「船に詰める荷物の限度」なのか「乗船できる人数の限度」なのか判断できません。capacityには定員という意味もあるからですね。
ということで「船荷の限度」とするとこうなります。
public class ship { long loadingCapacity; }
「船荷の」という修飾語を付けたことで「船に詰める荷物の限度」であることが明確になりました。 もう「loadingCapacity」が乗船できる定員であるかも?と悩む人はいません。
「より具体的な表現を探す」
ここから更にもう一歩踏み込んでみましょう。「船荷の限度」をもっと明確にした言葉はないのでしょうか?
あるんです!もっと明確に表現する単語が!!
public class ship { long burden; }
「burden」は明確に「(船の)積載量」を表す単語です。「船荷の限度」とは「(船の)積載量」を指しているわけですね。
「背の高さ = 身長」、「体の重さ = 体重」のように、「hogeのfuga」と修飾語で表現している場合、もし明確にその言葉を表す単語が存在するようであればそれを使いましょう。より明確な名前となり誤解を生む余地が減ります。
リーダブルコードの具体例
リーダブルコードに記載されている具体例を引用します。
def GetPage(url);
「get」は取得することしか表現していません。引数でurlを入力してインターネットから取得するのなら「fetchPage()」や「DownloadPage()」の方が明確です。
class BinaryTree { int size(); };
二分探索木の「size()」が何を返すのか分かりません。ツリーの高さなら「height()」、ノードの数なら「numNodes()」、メモリ消費量なら「memoryBytes()」の方が明確です。
class Thread { void stop(); };
この「stop()」が取消ができない重い操作なら「kill()」、後から再開できるのなら「pause()」にすると「どのように止まるのか」が明確です。
2.3 抽象的な名前よりも具体的な名前を使う
本記事ではこの言葉を以下のようにブレイクダウンしました。
「手段や属性ではなく目的、機能、挙動を名前で表す」
なぜ手段や属性を名前に使わない方が良いのか
手段や属性は目的を実現する一部であり、それだけでは目的を表現しきれないからです。
例として、Javaのロギング処理ではログレベルとして「debug / info / warn / error / fatal」の5つがあり、それぞれが明確に目的を表しています。
# | レベル | 目的 | 実用例 |
---|---|---|---|
1 | debug | デバッグに利用する詳細な情報 | 引数で受け取った値の全情報 |
2 | info | 後で閲覧する可能性がある事象 | ユーザのログイン・ログアウト |
3 | warn | エラーではないが例外的な出来事 | 存在しないユーザIDでログイン試行 |
4 | error | 予期しない実行時エラー | 非チェック例外の発生 |
5 | fatal | 異常終了を伴う致命的なエラー | プロセスダウン |
ここで「logger.badLog()」というメソッドを作成した場合、どのレベルが含まれるでしょうか? 「badLog()(悪いログ)」と言われると「fatal、error」は含まれそうですが「warn」が含まれるかは悩ましいです。
「good、bad(良い、悪い)」という属性を名前に利用すると、この「bad(悪い)」だけでは目的が明確にならないのです。
もしかしたら、「ログフォーマットがおかしい、ログの本文が無い」などログとして品質が良くないという意味で「bad(悪い)」を使っているのかもしれません。 そうすると「badLog()」メソッドを利用しようとしている開発者の想定と全く違う動きをします。
名前を呼んで何をしているか?が伝わるようにするためには「目的、機能、挙動」を名前で表しましょう。
リーダブルコードの具体例
リーダブルコードに記載されている具体例を引用します。
属性が名前になっていて明確ではない
C++でメモリリークを防止するために「コピーコンストラクタ」と「=演算子のオーバーロード」をprivateにして呼び出せないようにしています。
class ClassName { private: DISALLOW_EVIL_CONSTRUCTORS(ClassName); public: ... };
しかし名前が「EVIL(悪の)コンストラクターを許可しない」となっており、「何が悪で許可されていないのか?」、「何は許可されているのか?」が明確になっていません。
そのため、より具体的な名前に変更されました。変更後の名前は「COPY(コピー)」と「ASSIGN(代入)」が許可されていないことが明確になっています。
DISALLOW_COPY_AND_ASSIGN(ClassName);
手段が名前になっていて明確ではない
ローカルPCの開発時により詳細なデバッグ情報を出力するために「--run_locally(ローカルで実行する)」オプションを用意しました。
しかし「--run_locally(ローカルで実行する)」は手段であり、「より詳細なデバッグ情報を出力する」という目的を表していません。
そのため、新しいチームメンバーはローカルで開発する際に「--run_locally」オプションを使用していたものの、このオプションがなぜ必要なのか?このオプションで何が起きるのか?は把握していませんでした。
目的で命名するなら「--extra_logging」、機能で命名するなら「--debug_logging」にするとより明確になります。
オプションを入れるとロギング処理が重くなるから開発時にしか使用して欲しくない。という場合は「--debug_logging」の方が「デバッグで使う」という意図が伝わります。
障害発生時などに、本番に適用して調査に利用できる実装になっている。という場合は「--extra_logging」の方が「ログ情報を追加する」という意図が伝わります。
2.4 名前に情報を追加する
本記事ではこの言葉を以下のようにブレイクダウンしました。
「定数名、変数名、引数名に単位を入れる」
「定数名、変数名、引数名に重要な属性を入れる」
単位を入れる
利用する値に単位が存在する場合は定数名、変数名、引数名に単位を入れましょう。
以下のメソッドはJavaのORM「MyBatis 3」でタイムアウト時間を設定するものです。
void setDefaultStatementTimeout(Integer defaultStatementTimeout)
この引数にはどの単位で入力すれば良いでしょうか。マイクロ秒?ミリ秒?秒?分?
void setDefaultStatementTimeout(Integer defaultStatementTimeoutSec) void setDefaultStatementTimeout(Integer seconds)
こんな感じで引数に単位が入っていれば悩むことなく利用できます。
重要な属性を入れる
利用する値に紐つく重要な属性がある場合は、定数名、変数名、引数名に入れましょう。
パスワードを格納する変数を用意してみます。
String password;
パスワードを格納する変数であることだけ分かります。ユーザが入力したパスワードなのか?DBなどから取得したパスワードなのか?も分かりません。
String hashedEnteredPassword;
ユーザが入力したパスワードがハッシュになっていることが分かります。なので、この後どこかに保存されているハッシュ済みパスワードとの照合が必要と予想できます。
さらに、このシステムではパスワードをハッシュで管理しているので、パスワードに対して暗号化、複合を行っていないことも読み取れます。
リーダブルコードの具体例
リーダブルコードに記載されている具体例を引用します。
値の単位
# | 関数の仮引数 | 単位を追加した仮引数 |
---|---|---|
1 | Start(int delay) | delay → delay_secs |
2 | CreateCache(int size) | size → size_mb |
3 | ThrottleDownload(float limit) | limit → max_kbps |
4 | Rotate(float angle) | angle → degrees_cw |
その他の重要な属性を追加する
# | 状況 | 変数名 | 改善後 |
---|---|---|---|
1 | passwordはプレインテキストなので処理する前に暗号化すべきである | password | plaintext_password |
2 | ユーザが入力したcomentは表示する前にエスケープする必要がある | comment | unescaped_comment |
3 | htmlの文字コードをUTF-8に変えた | html | html_utf8 |
4 | 入力されたdataをURLエンコードした | data | data_urlenc |
2.6 名前のフォーマットで情報を伝える
本記事ではこの言葉を以下のようにブレイクダウンしました。
「言語の規約、現場の規約を把握して守る」
言語ごとに規約(コーディングルール)がありますので守りましょう。 また、現場でも規約(コーディングルール)を定めていることが多いので守りましょう。 規約を守ってコードを書くことで、統一感のある一貫性を持ったコードとなり理解しやすくなります。
規約のチェックはコンピュータにやらせる
規約を丸暗記して守るのはさすがに無理があります。そもそも、そんなところに人間の貴重なリソースを使いたくありません。
なので、規約が守られているかのチェックには静的解析ツールを活用しましょう!
# | 種類 | 機能 |
---|---|---|
1 | Linter | 文法の誤り、バグの原因となりやすい記述などをチェックして警告 |
2 | Formatter | ソースコードを自動整形してフォーマット差異が出ないようにする |
3 | SpellChecker | スペルミスを検出して警告 |
規約が守れていない場合は静的解析ツールに怒ってもらえば人間は楽ができます。 ついでに、CI/CDに組み込んでしまえば静的解析ツールの警告を無視してマージされることも予防できます。素晴らしいですね!
上記以外にもバグ検出、品質チェック、脆弱性チェックなど静的解析ツールは色々とあります。プロジェクトにマッチするものは活用して人間が楽できるようにしたいですね。
筆者がJavaで利用した経験がある静的解析ツールはこんな感じです。
# | 種類 | ツール名 |
---|---|---|
1 | Linter | Checkstyle |
2 | Formatter | Eclipseフォーマッタ |
3 | SpellChecker | Eclipseスペルチェック |
4 | バグ検出 | SpotBugs |
5 | 品質チェック | Coverity |
6 | 脆弱性チェック | SonarQube |
静的解析ツールに限らず、自動化できる部分は自動化したほうが楽ができますし、ヒューマンエラーが予防できます。
過去にAWS Lambdaを利用したWebアプリ開発の際、追加したAWS Lambdaが出力するCloudwatchに対して監視設定の追加が頻繁に漏れていました。
そのため、Githubにpushした際にCloudFormationのテンプレートをチェックしてAWS Lambdaに対応するCloudwatchの監視設定が存在しない場合は「hoge LambdaにCloudwatchの監視設定がありません」とエラーを出力してGithubActionsがこけるツールを作りました。
まとめ
本記事では、リーダブルコード2章からすぐに実行できて効果が高い部分を解説しました。
「2.6」の静的解析ツールはプロジェクト運用が絡むので「すぐに」とはいかないかもしれません。
しかし、それ以外は明日から即実践できるものになっています。まずはできる範囲から取り入れてみてください。
余談ですが、昔の仕事でDBを検索する「findAll()」メソッドの中にwhere句が書かれていたことがありました。本番障害の対応でソースコードを読んでいたのですが、何度読んでも原因が特定できないまま半日が経過しました。しょうがなく全てのメソッドの中を読み始めたのですが「findAll()」しない「findAll()」メソッドが原因でした。