Clean Code読書メモ
概要
Clean Codeを読んだのでその際にとったメモです。
意味のある名前
- 意図が明確な名前ににする
- 偽情報を避ける
- 意味のある対比を行なう
- 発音可能な名前を使用する
- 検索可能な名前を用いる
- メンタルマッピングを避ける(コードを読む人が心の中でその人が知っている既知の名前に変換しなければならない状況を作らない)
- クラス名には動詞をつけるべきではない
- メソッドには動詞、動詞句を付ける
- 1つのコンセプトには1つの単語
関数
- 関数は小さい事が重要(20行以上の長すぎる関数を作成しない)
- 関数は1つの事を行なう(関数の名前で示される、ある1つの抽象レベルに置けるいくつかのステップのみで表現されるのならその関数は1つの事をしている)
- 1つの関数に対して1つの抽象レベル
- 関数は抽象レベルの順番に並べる(コードは上からしたへと物語のように読める必要がある)
- 内容をよく表す名前を使用する(内容をよく表す長い名前は不可解な短い名前よりも優れている)
- 関数の引数は少ない方が良い(3つ以上は避けるべきで、4つ以上は余程の理由がない限りやめるべき)
- 1引数の場合関数名と引数は洗練された動詞・名詞の組み合わせになる(例: writeField(name))
- 副作用を避ける(関数が1つの事を行なう事を保証しつつ、隠れて別の事を行なう事を避ける)
- コマンド・照会分離の原則(関数は何らかの処理を行なうか、何らかの応答を返すかのどちらかを行なうべきであり、両方を行ってはならない)
- try/catchブロックの分離(try/catchブロックはそれ自体が不恰好であるので、ブロックを関数として外に出した方が良い)
- エラー処理も1つの処理(エラー処理を行なう関数は他の事を行なうべきではない)
- DRY原則(各所に重複するコードがあるとアルゴリズムに変更が必要な場合に全ての箇所を修正する必要がある)
コメント
前提
コメントでダメなコードを取り繕うことはできない(コメントに頼らずコードをきれいにする = 書かずに済ますよりも優れたコメントはない)
前提を踏まえた上で有益になりえる良いコメント
- 曖昧な引数、戻り値の意味を明確にするコメント
- 結果に対する警告
// 時間がある時以外は実行しないでください
describe('MergeSort', () => {
it('sorts the array in the right order', () => {
const data = [8, 4, 3, 1, 2, 6, 5, 7]
expect(mergeSort(data)).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8])
})
)}
- TODOコメントa
前提を踏まえた上で良くないコメント
- 他の人が理解しづらい乱雑・冗長なコメント
- 誤解を招きかねないコメント
- コードで明らかなことを改めて伝えているノイズコメント
- コメントアウトされたコード
書式化(コードの読みやすさ)
- 行数の多すぎるファイルを作らない
- 新聞のように、見出し → 概要 → 詳細の流れを意識する(モジュール名は見出しで最初には高レベルの概念とアルゴリズム、そして下にいくにつれて詳細度が増す)
- コード内で意味のある塊ごとに空行を入れて読みやすくする
- ローカル変数を宣言する際はその変数が使用される場所となるべく近い位置で行われるべき
- インスタンス変数はクラスの頭で宣言すべき(インスタンス変数は多くのメソッドで利用される為)
- コード内で依存関係が存在する場合呼び出し側を呼び出される側の上に置くべき
- 概念的に結びつきが強いものは近くに配置する
- チーム内では1つの規則を決め、全ての人がその規則に従うべきである
オブジェクトとデータ構造
データ・オブジェクトの非対称性
- オブジェクトは裏にあるデータを隠して抽象化し、データを操作する機能を公開する
- データ構造はデータを公開し、意味を持った機能は何も提供しない
手続き型(データ構造を使用するコード)は新たな関数を既存のデータ構造に影響を与えずに追加することが可能。
オブジェクト指向の場合、依存の関数を変えることなく、新たにクラスを追加することが可能。
デルメルの法則
オブジェクトを使用する場合、そのオブジェクトの内部について知るべきではないという法則。
エラー処理
- エラー処理が本来のロジックを不明瞭にしてしまっていたらやり方が間違っている
- 最初にtry-catch-finally文から書き始める事でコード内にスコープができるのでTDDがやり易くなる
- nullをメソッドの引数として極力渡さない
例外で状況を伝える
例外にはエラーの場所と原因を判断できるコンテキストを持たせる必要がある。
スタックトレースだけでは失敗した処理の意図がわからないので、失敗した処理、失敗の種類等を含んだエラーメッセージを作成し例外に含める。
呼び出し元が必要とする例外クラスを定義する
アプリケーションの中で例外クラスを定義する場合は、それがどのようにキャッチされているかが重要(=無闇に細かくエラーをキャッチしてスローすれば良いという訳ではない)。
例えば、サードパーティのライブラリで様々な種類のエラーをキャッチする為に下記のような実装が考えられる。
try {
/*
処理
*/
} catch(err) {
if(err instanceof ErrorA) {}
if(err instanceof ErrorB) {}
if(err instanceof ErrorC) {}
if(err instanceof ErrorD) {}
}
場合によっては上記も良い実装だが、発生する例外によらず呼び出し側はほぼ同じ処理を行なう場合がある。
そのような場合は、ライブラリをラップしてしまい共通の例外を返す事がベストプラクティスの1つである。こうする事でライブラリへの依存をなくす事でライブラリの乗り換えも簡単にでき、モックの作成も容易にできるようになる。
正常ケースのフローを定義する
下記のようにエラー処理が通常処理が本来のロジックを不明瞭にしてしうケースはよくない。
try {
let expense = report.getExpense(employee.ID)
} catch(err) {
console.log(err)
if(err instanceof ExpenseNotFound) {
let epense = 0
}
}
境界
- 外部ライブラリをシステム内で無闇に使い回るべきではなく、必要になるクラス内もしくはいくつかの強い関連を持ったクラス内での使用に留めるべきである。
- 外部との接続テストによって外部との明確な境界を担保すべきである。それによって外部ライブラリのバージョンアップ等をストレスなく行なうことができる。
- 自分たちのコードの広範囲の箇所が外部のコードの詳細に関する知識を持つことは避けるべきである。
単体テスト
前提
テストは製品コードの柔軟性・保守性・再利用性を担保し高める。それゆえ製品コードよりも重要であると言うこともできる。
TDD三原則
第一原則: 失敗する単体テストのコードを書く前に製品のコードを書いてはならない
第二原則: コンパイルが通り、適切に失敗する単体テストができるまでは次の単体テストを書いてはならない
第三原則: 現在失敗している単体テストが通るまで、次の製品コードを書いてはならない
テストをきれいに保つ
- 汚いテストを持つということはテストを持たないのと同値である。なぜなら、テストは製品コードと共に変更していかなければならず、テストが汚いと変更は困難になるから。
- 単体テストは脇役などではなく製品コードと同様に重要であり、テストを書く為に熟考・設計・配慮が必要となる
クリーンテスト
- 洗練されたテストを作り上げるのは読みやすさが非常に重要である。テストコードの読みやすさは製品コードの読みやすさ以上に重要となり得る。
- 1つのテストには1つのアサートが好ましい(複数あっても良いがアサート文はできるだけ少ない方が良い)
- 1つのテストでは1つの概念のみを扱うべきである
F.I.R.S.T
クリーンテストをまとめると上記の頭文字に示される5つの規範に従う
- 高速である(Fast): テストは高速であるべき。テストの実行に時間がかかると頻繁に実行しなくなる
- 独立している(Independent): テストはお互いに関連すべきではない。あるうテストが後続テストの前提条件を準備してはならず、全てのテストは独立しており、順不同で実行可能であるべき
- 再現性がある(Repeatable): テストはどんな環境でも再現可能でなければならない。
- 自己検証可能(SelfValidating): テストの結果は成功か失敗かの2択にすべきである。テストの合否が明確にわからないと失敗の判定が属人化してしまう。
- 適時性がある(Timely): テストは必要な時にすぐに書かなければならない。製品コードを書く直前に単体テストを記載し、そのテストが通るように製品コードを書く必要がある。
クラス
- publicな関数から利用されるprivateなユーティリティ関数を書く事で、新聞記事を読むようにコードを読むことができる
- クラスは小さく保たなければならない
- クラス名はそのクラスの責務を表すべきである。クラス名があいまいなほどその責務が多くなりがち
- SOLIDの原則に従う
Discussion