Open13

良いコード悪いコードで学ぶ設計入門読書メモ

Keiichi NomuraKeiichi Nomura

第1章 悪しき構造の弊害を知覚する

  • 設計をないがしろにすることの弊害を理解する
    • コードが読み解けない
    • バグを生みやすい
    • 悪しき構造をさらに誘発
  • データを保持するだけのデータクラスと、別のクラスで計算ロジックが実装されてると起こりうる弊害
    • 同じ計算ロジックが複数箇所に分散しがち
    • たとえ一箇所に共通化できてたとしても、そのクラスに気づかない別の担当者がまた同じロジックを作りがち
    • データとロジックが分散する構造を「低凝集」という
    • 重複コードが生まれやすい
    • 重複コードによる仕様変更時の修正漏れも起きやすい
    • 関連するロジックを探すのに時間かかる(可読性低下
    • 未初期化状態(生焼けオブジェクト)によるNPE発生など
    • 不正値を混入しやすい
      • データクラス利用側でバリデーション入れたとしても、他のクラスでも利用してて重複コードに気づかず修正漏れも起こりうる
  • 弊害を知ることで、「何か対処しなくては」という意思が生まれる
Keiichi NomuraKeiichi Nomura

第2章 設計の初歩

  • 変数名は意図が分かる名前に
  • 目的ごとに変数を用意する
    • 同じ変数への再代入はコードの途中で変数の用途が変わるのでバグを生みやすい
  • 意味のあるまとまりでロジックをまとめメソッド化する
    • 一連の処理が長いとどこからどこまでが何の処理か分かりづらい
  • 関連するデータとロジックは一つのクラスにまとめる
    • クラスのインスタンス変数と、インスタンス変数を操作するメソッドをまとめる
    • コンストラクタで、インスタンス変数の不正値を弾く
Keiichi NomuraKeiichi Nomura

第3章 クラス設計

  • クラス単体で正常に動作するように設計
  • 良いクラスの構成
    • インスタンス変数
    • インスタンス変数を不正状態から防御し、正常に操作するメソッド
  • ↑の構成は必ずセットにする。どれか1つ欠けてもダメ
    • 例外はある
  • 他のクラスに初期化してもらったり、データの入力をチェックしてもらうクラスは安全に利用できない(未熟なクラス
  • 自己防衛責務をすべてのクラスが備える
  • 成熟したクラスにするには
    • コンストラクタで確実に正常値を設定
    • 計算ロジックをデータ保持側に寄せる
    • インスタンス変数は不変(イミュータブル)
      • インスタンス変数が上書き可能だと、いつ変更されたか、今の値がどうなってるかをいちいち気にしないといけない
    • インスタンス変数を変更したい場合は新しいインスタンスを作って返す
    • メソッド引数やローカル変数も不変
    • 「値の渡し間違い」を型で防止
      • 金額(int)を加算するメソッドの引数に金額ではなく、チケットの枚数(int)を間違えて入れてしまった、などが起こりうる
        • 金額加算のメソッド引数の型として独自の型を用意(Moneyクラスなど)
    • 現実の営みに無いメソッドは追加しない
      • 金額に加算減算、割引計算はあるけど、乗算はあり得る?
      • 仕様にない不必要なメソッドを善意で追加しない
  • 上記は「完全コンストラクタ」+「値オブジェクト」の設計パターン
Keiichi NomuraKeiichi Nomura

第4章 不変の活用

  • 可変(ミュータブル)と不変(イミュータブル)を適切に設計しないと悪魔が暴れだす
    • 想定した値と異なる値に変わっていた、といったように挙動の予測が困難
  • 再代入を許し、代入される値の意味が変わってしまうとコードの読み手が誤解する可能性もある(バグを埋めやすい
  • 可変インスタンスを複数クラスで使いまわした場合の弊害
    • 一方のクラスでインスタンス変数を変更すると他のクラスの値まで変わってしまう
  • メソッドでインスタンス変数の値を変更することの弊害
    • 別スレッドで異なるインスタンス変数操作があり、期待した値を得られない
    • 処理の実行順に依存してしまうと結果の予測や保守が難しくなる
  • 関数の影響範囲を限定する
    • 引数で状態を受け取る
    • 状態は変更しない
    • 変更後の値は関数の戻り値で返す
  • インスタンス変数は不変にする
    • 値を変更する場合は、新しい値を持ったインスタンスを返す
  • 可変にして良いケース
    • 大量のインスタンス生成が発生する、リソース制約の厳しい環境などでパフォーマンスに影響する場合
    • スコープが局所的なケース
      • ループ処理のスコープでしか使われない、など
  • インスタンス変数を可変にする場合は、正しい状態変更のみ発生させるメソッド(ミューテーター)を用意する
  • コード外とのやりとりは局所化する
    • ファイル読み書きやデータベース操作など、コードの外の状態に依存する操作の場合
      • 別のシステムによって書き換わりうるので、コードのみで完全にコントロールできない
    • リポジトリパターンなど
Keiichi NomuraKeiichi Nomura

第5章 低凝集

  • 凝集度
    • モジュール(クラス)内におけるデータとロジックの関係性の強さを表す指標
  • データクラスとセットで static メソッドで計算ロジックを別に定義しがち
    • インスタンス変数を使って計算する構造に設計すれば高凝集となる
  • インスタンス変数を使っていないメソッドは、 上述の static メソッドと変わらない
  • 単純なコンストラクタを公開すると、初期化ロジックが分散しがち
    • 初期化する値を変更したいとき、関連するソースを全部見直さないといけない
    • private コンストラクタとファクトリメソッドで目的別に初期化メソッドを作る
    • 初期化処理が膨大になるようなら別のファクトリクラスを検討
  • 高凝集な設計であれば、再利用性が高まる
  • 横断的関心事(ログ出力、例外処理など)であれば共通処理として static に設計して良い
  • 出力に用いる引数(出力引数)は、データ操作対象と操作ロジックが別定義になるので低凝集となる
    • 使うメソッドの引数は入力でしか使ってないのか、出力にも使っているのかがメソッドの中身まで読み込まないとわからなくなる
  • 多すぎる引数は、そのメソッドの処理内容が膨らんでるとみなせるので見直す
    • プリミティブ型を引数・戻り値に用いてばかりだと、コード重複が起きやすい
    • 3章の値オブジェクトなど使って、意味のある単位でクラス化する
  • メソッドチェイン
Keiichi NomuraKeiichi Nomura

今のプロダクトにこの本の内容を適用するなら、まずは課題をチーム内で明確にして共有できてること。出た課題に対して、どのようなアプローチが良いかを考える。その際に書籍が参考になるなら、って感じかな。
(これもスペース内であった話)

Keiichi NomuraKeiichi Nomura

第6章 条件分岐

  • if,switch 文をずさんに扱うと開発者を苦しめる
    • コードの見通しが悪くなると理解が困難になり、仕様変更時にバグを生みがち
  • 早期 return で条件分岐のネストを解消する
    • 条件ロジックと実行ロジックを分離できる
  • enum でタイプ別の処理をする switch 文は重複しがち
    • 単一責任選択の原則に従い、 switch 文の条件は1箇所にまとめる
    • 1箇所にまとめるとコード量が膨大になる場合は interface を使う
      • 共通で呼び出したいメソッドをinterfaceに定義する
    • switch 文の代わりに Map を利用(ストラテジパターン
      • enum の各値をキーに、interface 実装クラスを取得できるようにする
    • interface 各メソッドの戻り値がプリミティブよりは値オブジェクトで型定義できてるとさらに変更に強い構造に
  • 同じ判定ロジックがあちこちに書かれる懸念
    • ポリシーパターンで条件を集約
      • interface を利用してルールをメソッドで表現
      • ポリシークラスにルールを集約、そのルールをすべて満たしてるかを判定
  • 型チェック(instanceof)で分岐してる場合は interface で条件分岐削減の意味が無い
    • リスコフの置換原則に反する
    • interface のメソッドに、条件分岐に伴う詳細を表現する
  • フラグ引数も、その引数の値によって何が変わるのかを見るにはメソッド内部まで見に行かなければならない(可読性低下
    • フラグによって分岐する処理はメソッドごと分離する
      • メソッドは単機能となるように設計する
    • ストラテジパターンを使う
Keiichi NomuraKeiichi Nomura

第7章 コレクション

  • 自前でコレクション処理は作らず、標準ライブラリに頼ろう
  • ループ処理中の条件分岐ネスト問題
    • 早期 continue / break を使おう
  • コレクション処理に関する処理があちこちに実装される(低凝集)問題
    • ファーストクラスコレクションで、コレクション処理関連ロジックをカプセル化
      • コレクション型インスタンス変数
      • コレクション型インスタンス変数を不正状態から防御し、正常に操作するメソッド
  • 外部へコレクション型の値を渡す場合は、要素の変更を不可能にする
    • そのまま渡すと、外部でコレクション型要素の追加削除を許してしまう(副作用を生む
    • Java の List なら unmodifiableList を使う