# リーダブルコード 第Ⅳ部 選抜テーマ ## 14章 テストと読みやすさ ### 14.1 テストを読みやすくて保守しやすいものに **テストが読みやすいと、本物のコードも理解しやすくなる。** テストが大きくて読みにくいと、本物のコードも修正しにくくなり、テスト自体が嫌になって新しいコードでテストを書かなくなってしまう。 ### 14.2~14.3 テストコードを修正する #### テストを読みやすくする テストの設計原則として「**大切でない詳細はユーザから隠し、大切な詳細は目立つようにすべき**」である。 **<×ダメポイント>** * 書式設定等の重要でないものが先頭に来ている。 * URLをダラダラと繰り返している。(重複している。) * 不要なインデックス番号が目立つ。 **<○マルポイント>** * 先頭にはテストする**各要素をまとめた関数を定義**する。 * URLは1か所にまとめ、**重複を排除する。** * インデックスを使わずシンプルに。 #### 最小のテストを作る ミニマムなテストを作るには、まずやりたいことを言葉に起こす(12章参照) **コードの目的は何かを把握してから**実装に取り掛かると、最小限のテストができる。 こうして**独自の「ミニ言語」** を定義すると、**小さな領域で多くの情報が表現できる。** 定義したものを後で呼び出せるようにしておけば、最小限の範囲でテストが製作できる。 ### 14.4 エラーメッセージを読みやすくする デフォルトのエラーメッセージよりも分かりやすくする方法として、 1つはassert()メソッドを使う方法がある。カッコ内にチェックする条件を書いておくと、**エラー発生時に実際にどの部分が条件に反しているのかが一目でわかる。** もう1つは独自のエラーメッセージを定義する方法がある。**判定する条件に応じて独自の振る舞いを見せるエラー処理を定義することで、自分好みのエラーメッセージを印字できる。** **エラーメッセージはできるだけ役に立つように!** ### 14.5 テストの適切な入力値を選択する テストに入力する値は、適当では意味が無い。**条件を踏まえ、コードを完全にテストする最も単純な入力値の組み合わせを選択する。** たとえば任意の負の値1つ以上でよければ1桁の-1等でよく、複数個は必要ない。非常に大きな負の値であれば、-1e100のような簡潔な形にする。 入力値は大量であるほどバグ検知に役立つ。ストレステストレベル(約10万個以上)の入力値作成等、手動作成に負えない場合はプログラムで自動化すればOK。 また、コード検証のための完璧な入力値を1つ作るのではなく、**別々の見方をした小さなテストを複数作る方が簡単で効果的で読みやすい。** ### 14.6 テストの機能に名前をつける テスト機能の変数名は、テストするクラスや関数、あるいは状況やバグといった名前を付けておくと、すぐに理解しやすくなる。 Testは接頭辞としておき、情報をひとまとめにすると良い。 (例Test_SortAndFilterDocs()) また、テストの機能は他の機能に呼び出されることは無いため、長い名前になっても状況で分割するなどの説明的な名前の方が良い。 (例Test_SortAndFilterDocs_BasicSorting(),   Test_SortAndFilterDocs_NegativeValues() 等) ### 14.7 テストコード修正のまとめ 14.2で取り上げたコードは以下の通り修正点が挙げられる。 1. 書式設定等のどうでもいい情報が多い。 2. テストが簡単に追加できない。 3. 失敗メッセージが、デバッグできるレベルでは無く使えない。 4. 一度にすべてやろうとしている。一度に一つのことを。 5. 入力値が単純でない。 6. 入力値が不完全である。その入力値では検証が不十分。 7. 極端な入力値(空(null), 巨大な値等)でテストしていない。 8. Test1というありふれた名前のテスト名で、詳細が見えてこない。 ### 14.8 テストに易しい開発 **あとでテストを書くつもりでコードを書くと、テストしやすいようにコードを設計するようになる。** テストに易しい設計をすれば、振る舞いごとにうまく分割されて自然にコードが構成される。 なお、TDD等の方法論とは異なるものがあるが、**テストを気にしてコードを書けばコードの質が良くなるのは同じである。** #### テスト容易性の低いコード * **グローバル変数のあるコード** * グローバルの状態をテストごとに初期化する必要がある。 * どの関数にどんな副作用があるのか分かりにくい。 * **外部コンポーネントに依存しすぎているコード** * 最初に足場を設定しなければならず難しい。 * 依存しているものが使えなくなったらOUT。 * **コードが非決定的な動作をする** * テストが当てにならない。 * プログラムの競合や再現不可能なバグの発生が起きやすくなる。 #### テスト容易性の高いコード * **クラスが小さいコード** * テストやセットアップがしやすい。 * 単純明快で理解しやすい。 * **各クラスのタスクが1つである** * テストケースが少なくて済む。 * 小さく単純なコンポーネントがモジュールされ、疎結合である。 * **他のクラスにあまり依存していない** * 各クラスが独立してテスト可能である。 * 開発面ではクラス毎の並列作業が可能である。 * 変更が容易である。 * **関数が単純で、インターフェースが明確である** * 単純で明確な動作をテスト可能である。 * インターフェースが再利用可能である。 ### 14.9 やりすぎに注意 テストは、**あくまで本番コードの質を担保するためのもの**であり、テストのためだけに本物のコードを犠牲にしてしまうようでは本末転倒である。 また、テストのカバレッジ100%を目指す必要はなく、**90%をテストすればよい**のである残り10%はUIやどうでもいいエラーが含まれる。また、現実的に100%のテストカバレッジは無い。 また、プロジェクト全体がテストに支配されてしまうのも良くない。 ## 15章 分/時間カウンタを設計・実装する この章では、「分/時間カウンタ」のプログラムを題材に、これまでの総合知識を確認する。 ### 15.1 問題点 このプログラムでは、ウェブサーバの直近の1分間と、直近1時間の転送バイト数を把握するものである。だが、効率的な解決は簡単ではない。 ### 15.2 クラスのインターフェースを定義する #### 名前をカイゼンする クラス名を決定する。3章の「誤解されない名前」を意識する。 * getは軽量アクセサという認識が多いため使わない方が良い。 * Count()は、これをカウントしてほしいのか、それともこれまですべてのカウントが欲しいのかで誤解を生じやすい。 * Add()は数値の追加とリストへの追加の意味を含む。 #### コメントをカイゼンする 見ればわかる処理の説明になっている部分が複数箇所ある。 機能としてのコメントにカイゼンしたり、実際の動作に合わせた動き(直近1分間→開始時刻によらず直近60秒であれば、直近60秒間)に修正したりするとよい。 ### 15.3 解決の試案1: 素朴な解決策 解決の試案として3つ紹介されている。 まず1つ目は、タイムスタンプ付きのイベントリストを保持する考え方である。 単純な考え方で解決策として正しいが、for文が複雑なのと、処理内容の重複が生じている。 カイゼンされたコードでは、重複が抽出されたほか、仮引数を絶対値にすることで扱いやすくした。また、逆向きのイテレーターのため、名前をritに変更した。課題のfor文は伝統的な形に戻して読みやすくした。 この試案はなお問題のあるコードでもある。 * 全ての処理が1クラスのため、さらに大きくしているとメモリがいっぱいになる。 * 呼び出しで経由するデータ量が多すぎて、動作が重くなっている。 ### 15.4 解決の試案2: ベルトコンベヤー設計 上記の問題を解決するため、listをベルトコンベヤーのように使う方式を考える。新しいイベントはベルトコンベヤーに載せられ、1時間(1分)経過後にコンベヤーから落下し自動削除される。データの新鮮さを比較して合計の量を加減していく。 ベルトコンベヤー方式には2つある。イベントの効率性を考えると後者に軍配が上がる。 * イベントをコピーし、1時間ベルトと1分ベルトにそれぞれイベントを載せる方式 * 1分ベルトの延長線上に1時間ベルト(59分ベルト)を置きイベント1つで両方を動かす方式 minute_countとhour_countの更新処理及び自動削除処理は、ShiftOldEvents()に抽出してまとめておく。 ベルトコンベヤー設計により、上記の問題は解決した。しかし、いくつかの面で課題が残る。 * ShiftOldEventsが、非常に密度の濃い関数であるため、追加機能に対応する柔軟性に欠ける。 * メモリ消費量が多く、動作が重くなる可能性が高い。 ### 15.5 解決の試案3:時間バケツの設計 このプログラムでは、time_tが秒数を保持しており、数値の丸め誤差が発生してしまう。ミリ秒の粒度を使えば解決するが、今回はそこまでは必要ない。精度とパフォーマンスはトレードオフであるため、今回は精度を落とす代わりに高速でメモリ使用量も少ないMinuteHourCounterを新しく作ることにする。 この構造は1分間を1つのバケツとして扱い、バケツ単位でイベントの合計値を出すことにする。MinuteCountやHourCountメソッドの精度は1/60となるが、メモリ使用量を固定化し予測可能とした。 まずは1種類の期間しか使えないMinuteHourCounterを汎化したTrailingBucketCounterを作成する。ポイントは現在時刻にカウントを依存している部分である。クラスを「時計無し」クラスにできるほか、呼び出しが1か所に固まるメリットがある。 精度を上げることになっても、バケツを増やせばよいので柔軟性も増した。 また、自動削除処理とminute_countとhour_countの更新処理を分割して考え、一度に一つのことをの原則を守っている。 ### 15.6 3つの解決策を比較する 時間バケツ設計では、コードの行数こそ増えたが、それはクラスを分割して読みやすくしている証左であり好ましい。計算量やメモリ使用量で動作が重くなりがちだったが、設計の工夫やトレードオフ関係の見直し等により改善した。 ###### tags: `読書`