📝

技術書読書ログ「Good Code, Bad Code」

2023/02/10に公開

個人的に重要だと思ったところ

サマリ

「Good Code」のための戦略は「コード品質の6つの柱」

この6つの柱を満たすためには、「きれいで明確な抽象化レイヤー」と「明示的なコードと暗黙的なコードの使い分け」が重要

コード品質の6つの柱

  • 「コードが読みやすくする」
  • 「想定外の事態をなくす」
  • 「誤用しにくいコードを書く」
  • 「コードをモジュール化する」
  • 「コードを再利用、汎用化する」
  • 「テストしやすいコードを書き、適切にテストする」

きれいで明確な抽象化レイヤー

システムが実行することには、大きい概念のものから小さい概念のものまで階層構造で表現できる。
例えば、「サーバーにメッセージを送る」という処理は以下のような構造になる

  • サーバーにメッセージを送る
    • HTTP通信を開く
      • HTTPセッションの確率
        • ...
      • ...
    • 文字列のメッセージを送る
      • ...
    • HTTP通信を閉じる
      • ...

この階層構造をコードで表現するのが「きれいで明確な抽象化レイヤー」

「きれいで明確な抽象化レイヤー」になっていると、コードが適切な大きさに切り分けられていて、適切に整理されている状態になり、6つの柱のうち「コードが読みやすくする」「コードをモジュール化する」「コードを再利用、汎用化する」「テストしやすいコードを書き、適切にテストする」の4つに繋がる。

システムが実行することを再帰的に小さく分割し、抽象化レイヤーを作る努力をすれば、個々のコードはそれほど複雑になることはない。

ただし、「きれいで明確な抽象化レイヤー」にするための絶対的な方法はない。システムが実行することはシステムによって異なるため。システムやドメインに対しての理解を深めることと、4つの柱を意識して設計の見直しとコードの書き直りを繰り返しながら作り上げるしかない。

レイヤー間の境界を強制的に明確にしたり、実装の詳細がそれぞれのレイヤーで漏れ出していないことを担保するためにインタフェースを定義することは有効なアプローチ。ただし使いすぎると複雑になるので多くのメリットが得られる時にだけ利用するのが良い。インタフェースがなくてもレイヤー間の境界を明確にしたり、実装の詳細が外に漏れないことを意識する。

明示的なコードと暗黙的なコードの使い分け

コードには明示的なものと暗黙的なものがある

  • 明示的 : 開発者や利用者が見落とすことがない
    • 関数やクラスの名前
    • パラメータの型
    • 戻り値の型
    • 検査例外
  • 暗黙的 : 開発者や利用者が見落とす可能性があるもの
    • コメントやドキュメント
    • 非検査例外

明示的なコードと暗黙的なコードの使い分ることができれば、「想定外の事態をなくす」「誤用しにくいコードを書く」に繋がる。例えば関数やクラスの使い方を制限したり強制させる際は、明示的なコードのほうが信頼度が高くなる。

また、エラーハンドリングでも明示的か暗黙的かを意識して使い分けると良い。

回復不可能なエラーが起きた際は「非検査例外」や「panic」「検査・アサーション」といった暗黙的だが目立つ方法を使うといい。回復不可能な場合は呼び出し元でできることはなく、エラーが起きたことをエンジニアに気付かせるため。

回復可能なエラーが起きた場合は「戻り値の型」や「検査例外」を使って明示的にして、エラーハンドリングを強制させるようにするのがおすすめ。

その他のTIPS

誤用しにくいコード

誤用しにくいコードの特徴は以下

  • 呼び出し元が不正な入力を渡しにくい
  • 副作用が起きにくい
  • 誤ったタイミング・順序で関数を呼び出せない
  • 想定から外れる変更がされにくい

これらを実現するためには以下が有効

  • 深い不変なコードにする
    • 例えばクラスであれば、そのメンバもできるだけ全部不変にしたり、防御的にコピーする
  • 汎用的なデータ型を避けて専用の型を使用する
  • データに対して情報源を信頼できるものだけにする
    • 1次データを使って計算できる派生データはなるべく作らない
  • 「ロジックに対して情報源を信頼できるものだけにする」が有効
    • 例えば、データの書き出しと読み込みをする処理があるときに、保存フォーマットへのシリアライズ・デシリアライズを別々で管理すると保存フォーマットがズレる可能性が出てしまうので、1箇所(1クラス)で管理する

テストコード

テストダブルの定義

  • モック
    • 関数の呼び出しを記録する機能を提供
    • 副作用が発生する依存をシュミレートする際に使う
  • スタブ
    • 関数を呼び出すたびに定義された値を返す
    • 戻り値を提供する依存をシュミレートする際に使う
  • フェイク
    • テスト用だけど、プロダクションの依存を正確にシュミレート
    • 多くは外部システム連携の代わりに、フェイククラス内の変数に状態を管理させる

テストコードを書く際は古典派の考え方がおすすめ

  • 可能な限りプロダクションの依存を使い、それができない場合の次善の選択肢がフェイク、モックとスタブは最終手段
  • 個々の関数にフォーカスしたテストではなく、個々の動作にフォーカスしたテストにする
    • 個々の関数にフォーカスすると、複数の関数に跨る動作のテストが漏れる可能性がある

テストのフィロソフィー・方法論(「TDD」「BDD」「ATDD」など)については、手段よりもゴールを重視する。適切で徹底的なテストを書いて、ソフトウェアの品質を上げるのがゴール。そのゴールが達成できれば、フィロソフィー・方法論に忠実に従っても従わなくてもどちらでも良い。

感想

初級者向けって書いてあったけど、理解はしていても現場できていないところがあったり、自分ではちゃんと言語化できていないところがあったりで、得られるものが多かった。

個人的には抽象化レイヤーの内容が一番響く内容でした。「きれいで明確な抽象化レイヤー」の一言で良いコードが包括されているように思えたのと、そしてそれを実現するのには一筋縄ではいかないのも共感できたので。今後、コードを書くときはこれを常に意識したい。

Discussion