読書ログ「良いコード/悪いコードで学ぶ設計入門」
良くない構造
- 意味不明な命名
- 技術駆動命名、連番命名
- ネスト深い
- 関連事項が別クラスで定義されてる(低凝集)
以前、この現場ほど汚いソースコードのところはないから、ここのコードわかればどこでも通用するよ、と言われた案件を思い出しました😅
設計基本
- 分かりやすい命名
- let で上書きしていったりせず、目的ごとの命名を
- メソッド化でまとめる
- クラスにはデータと関連ロジックをまとめる
リーダブルコードも読み返そう
クラス設計
「クラスが単体で正常動作するよう設計する」
重要な構成要素
下記2つが欠かしてはいけない構成要素
- インスタンス変数
- インスタンス変数が不正値になるのを回避し、正常に操作するメソッド
- コンストラクタ内で、カード節で不正値を弾く
具体的な設計
- インスタンス変数を不変(イミュータブル)に
- 変更する場合は新たなインスタンスを作成する
- メソッド引数、ローカル変数も不変に
- 引数の渡し間違いを型で防止
- 不必要なメソッド削除
チェックリスト
設計によって改善すべきこと
- 重複コード
- 修正漏れ
- 可読性低下
- 生焼けオブジェクト(未初期化のオブジェクト)
- 不正値の混入
- 思わぬ副作用(代入されたりなど)
- 値の渡し間違い
設計パターン
- 完全コンストラクタ
- 不正状態から防衛する
- 値オブジェクト
- 特定の値に関するロジックを高凝集にする
- ストラテジ
- 条件分岐を削減し、ロジックを単純化する
- ポリシー
- 条件分岐を単純化したり、カスタマイズできるようにする
- ファーストクラスコレクション
- 値オブジェクトの亜種で、コレクションに関するロジックを高凝集にする
- スプラウトクラス
- 既存のロジックを変更せずに安全に新機能を追加する
不変の活用
- 再代入を防ぐ
- 変数、引数を不変にする
- 影響範囲を限定する関数の設計
- データ(状態)を引数で受け取る
- 状態は変更しない
- 値は関数の戻り値として返す
- デフォルトは不変に
可変の取り扱い
- 変数を可変にして良い時
- パフォーマンスに問題が生じる場合
- スコープが局所的の場合
- 正しく状態変更するメソッド(ミューテーター)を設計する
- コード外とのやりとりは局所化する
- リポジトリパターン
- データソースの制御ロジックをカプセル化する
- リポジトリパターン
凝集
低凝集となる例
- staticメソッドの誤用
- staticメソッドはインスタンス変数を使えない
- そのため、データとロジックが別のクラスで定義されてしまう
- インスタンスメソッドのフリをしたstaticメソッドも存在
- インスタンス変数を使わずに引数のみを使っている
- staticメソッドはインスタンス変数を使えない
※ C言語などの手続き型言語の設計を踏襲してstaticメソッドが用いられている。凝集度に影響がなければstaticメソッドを用いてよい。
-
初期化ロジックの分散
- コンストラクタをpublicにすると自由にインスタンスの初期化が可能になる → あるインスタンス変数の初期値が変更になる際、同じような初期値にしたものもチェックしなければならなくなる
- privateコンストラクタ+ ファクトリメソッドで目的別に初期化するといい
- 「ファクトリメソッド」=「インスタンスの作り方をスーパークラスで定め、具体的な処理をサブクラスで行う」
- クラス内部でのみインスタンスを生成できる
- 目的別の生成ロジックが増えすぎた場合は、ファクトリクラスを検討する
-
共通処理クラスの使用
- 共通処理の置き場所として用意されたクラス
- staticメソッドで定義されることが多い
- 関連しないメソッドが定義されることも
- 横断的関心事であれば、共通処理としてまとめる
- 横断的関心事とは
- ログ出力
- エラー検出
- デバッグ
- 例外処理
- キャッシュ
- 同期処理
- 分散処理
- 横断的関心事とは
- 共通処理の置き場所として用意されたクラス
-
出力引数の使用
- 出力引数 = 結果を返すための引数(出力として用いられる引数) ex. 引数を変更して返す
- 重複して実装されやすい
- 引数が入力か出力か確認する必要が出てくる = 可読性低下
- 出力引数 = 結果を返すための引数(出力として用いられる引数) ex. 引数を変更して返す
-
多すぎる引数
- 引数がプリミティブ型だけで構成されるプリミティブ型執着 → コード重複が生じやすい
- データを引数として扱うのではなく、データをインスタンス変数として持つクラスへ設計すると良い
-
メソッドチェインの使用
- メソッドチェイン = 「 . 」でつなぎ、戻り値の要素にアクセスしていくこと
- 影響範囲が大きい構造
- オブジェクトの内部状態(変数)を尋ねたり、その状態に応じて呼び出し側が判断等をするのではなく、呼び出し側はメソッドを命ずるだけで命令された側で適切に処理するように設計すると良い =「尋ねるな、命じろ」
- インスタンス変数をprivateにして外部からアクセスできないようにし、メソッドを外部から命じることでインスタンス変数を制御するようにする
条件分岐
- ネストを避けるため、早期returnをする
- 条件ロジックと実行ロジックを分離
- switch文の重複を防ぐ
- 条件分岐を一箇所にまとめる
- 「単一責任選択の原則」= 「ソフトウェアシステムが選択肢を提供しなければならない時、そのシステムの中の1つのモジュールだけがその選択肢のすべてを把握するべきである」
- interfaceを用いて、よりスマートに
- interfaceを用いて処理を一斉に切り替える設計をストラテジーパターンと呼ぶ
- 異なる型を同じ型として利用できるようにする(instance of で条件分岐したりせずに同名のメソッドを呼び出せる)
- 分岐ロジックを書かずに分岐と同じことが実現可能
- さらに、enumとMapを用いることで、switch文を使わずに処理の切り替えが可能
- プリミティブな型ではなく、独自の型を定義し、値オブジェクト化するとさらに品質向上できる
- 条件分岐を一箇所にまとめる
- 条件分岐の重複とネスト
- interfaceを用いたポリシーパターンで、条件の部品化、部品化した条件を組み替えてカスタマイズ可能
- 型チェックで分岐しない
- 「リスコフの置換原則」= 「基本型を継承型に置き換えても問題なく動作しなければならない」を守ろう
- 中級への一歩
- 分岐は、interface設計を試みる
- 分岐ごとの処理は、クラス化を試みる
- 分岐を書きそうになったら、まずinterface設計!
- フラグ引数を避ける
- メソッドの機能を切り替えるboolean型引数をフラグ引数と呼ぶ
- 何が起こるか想像が難しい→可読性の低下
int型引数で切り替えるのも同じデメリットが - メソッドを分離する(単機能にする)
- 切り替え機構をストラテジパターンで実現する
- ストラテジーパターン(interfaceの利用) & enum, Mapで切り替える
コレクション(配列やリストなど)
悪いケースとその対策
- 自前でコレクション処理を実装してしまう(車輪の再発明)
- for文にif文がネストしたりする
- JavaではanyMatchなどコレクション用メソッドがある
- 標準ライブラリにメソッドがないか探してみよう
- ループ処理中の条件分岐ネスト
- 早期continue
- 早期break
- 低凝集なコレクション処理
- 実装が重複しがち
- コレクション処理をカプセル化する
- ファーストコレクション: コレクションに関連するロジックをカプセル化する設計パターン
下記の要素を備える- コレクション型インスタンス変数
- コレクション型インスタンス変数を不正状態から防御し、正常に操作するメソッド
- 外部へ渡す場合はコレクションを変更できなくする
- 例. unmodifiableList メソッドを使用する
- ファーストコレクション: コレクションに関連するロジックをカプセル化する設計パターン
密結合(他のクラスに依存している構造)
密結合と責務
あるクラスの仕様変更が他のクラスの実装にも反映されてしまう。ロジックの置き場所がちぐはぐなものは、責務が考慮されていないクラス。
従うべき原則
単一責任の原則
「クラスが担う責任は、たったひとつに限定すべき」
DRY原則の誤用に注意
DRY原則
「すべての知識はシステム内において、単一、かつ明確な、そして信用できる表現になっていなければならない」
DRYにすべきなのは概念。
同じようなロジック、似ているロジックであっても、概念が違えばDRYにすべきではない。
※ 似て非なるコードを共通化しないように
共通でないとわかった段階で共通化解消する
密結合の各種事例と対処方法
-
継承に絡む密結合
- 継承はよっぽど注意して扱わないと危険、密結合に陥る
- 無理にスーパークラスのロジックを共通化しようとした結果、密結合となる
- 基底クラスのロジックを知る必要がある(関連知識が1つのクラスに凝集しておらず、基底と継承それぞれに分散している)
- 無理にスーパークラスのロジックを共通化しようとした結果、密結合となる
- スーパークラス依存により、スーパークラスの変更に影響を受ける
- 継承より委譲(コンポジション構造)へ
- 利用したいクラスをprivateなインスタンス変数で持ち、呼び出す
- スーパークラスとサブクラスで、お互いの中身を知らなくてもよい、変更の影響が少ないクラス構造にする
- 継承はよっぽど注意して扱わないと危険、密結合に陥る
-
インスタンス変数ごとにクラス分割可能なロジック
- 責務の異なるメソッドは同じクラスに定義すべきではない
- 依存関係の図を描いてみる = 影響スケッチ
Jigなどのツールがある
-
なんでもpublicで密結合
- 関係し合ってほしくないクラスどうしが結合し、影響範囲が拡大する → アクセス修飾子で可視性を制御する
- クラスは標準的にprivateに
-
privateメソッドだらけ
- privateメソッドが多いクラスは多くの責務を持つことが多い → 責務の異なるメソッドは別々のクラスに分離
-
高凝集の誤解からくる密結合
- 高凝集を意図して強く関係しそうなロジックを一箇所にまとめようとしたものの、密結合に陥ってしまうケースが多い → 概念は分離する
- 表示関連クラスに表示以外の責務ロジックがある構造 = スマートUI(利口なUI) → それぞれ別のクラスにすべき
-
巨大データクラス
- グローバル変数と同様の弊害を招く
-
トランザクションスクリプトパターン
- 一連の処理手順が長く書き連ねられている構造
- データクラスとデータ処理クラスで分けられている場合に頻発する
-
神クラス
- トランザクションスクリプトパターンの重症化版
密結合クラスの対処法
- 単一責任の原則に従う
- 遵守するよう設計されたクラスは100〜200行となる
- 早期return
- ストラテジパターン
- ファーストクラスコレクションパターン
- 目的駆動名前設計にもとづく命名
etc.
よくないコード
- デッドコード
到達不能コード - YAGNI原則を破ったもの
先回りでロジックをつくる → 必要になったときにのみ実装すべし - マジックナンバー
意図不明な数値 → 定数で定義する - 文字列型執着
etc. 単一のString変数に複数の値をカンマ区切りで格納 → splitメソッドを使って値を取り出す- プリミティブ型執着が先鋭化し、クラスの追加、ひいては変数の追加まで嫌煙することによる → 意味の異なる値は別の変数へ格納しよう!
- グローバル変数
- 多くのロジックめグローバル変数を参照し値を変更していると把握が困難になる
- 排他制御で対応 → 設計の誤りによりデッドロックに陥る可能性あり
- 巨大データクラスもグローバル変数の性質を帯びやすい(多くの箇所から参照されやすいため)
- 影響範囲を最小化するように設計する
- null問題
- nullは、未初期化状態のメモリ領域へのアクセスが制御上トラブルの原因にる事態を防ぐために発明されたもの
- 未設定状態をnullと実装する場合が多い → 未設定状態も立派な状態
- nullを返さない、渡さない
- null安全
- null非許容型
- 例外の握り潰し
例外をキャッチしてもなんの処理もしていない - 設計秩序を破壊するメタプログラミング
メタプログラミング=プログラム実行時にプログラム構造自体を制御するプログラミング
ex. Javaのリフレクション- 利用する際、限定的に利用するなどして、リスクが生じないようにする
- 技術駆動パッケージ
設計パターンなど構造的に似ているものどうしでフォルダ分け、パッケージ分けをすること。MVCアーキテクチャでよく用いられる。- ビジネスクラス(ビジネス概念を表すクラス)を技術駆動パッケージングすると、ファイル単位で低凝集となる
- ビジネスクラスはビジネス概念で関係するものどうしが一緒になるようにする → 同フォルダのクラスをpackage privateにできる
- サンプルコードのコピペ
- サンプルコードとは離れて、あるべきクラス構造を設計しよう
名前設計
「目的駆動名前設計」
ソフトウェアで達成したい目的をベースに名前を設計する!
関心の分離
「ユースケースや目的、役割ごとに分離する」
大雑把で意味がガバガバな名前は、あらゆるロジックを引きつけてしまうので注意
プログラミングにおける名前の役割は、可読性を高めることだけではない
名前を設計する!
命名が重要であり、名前とロジックが対応する前提であること、名前がプログラム構造を大きく左右する
ポイント
- 可能な限り具体的で、意味範囲が狭い、特化した名前を選ぶ
- 存在ベースではなく、目的ベースで名前を考える
- どんなビジネス目的があるか分析する
- 声に出して話してみる
- ユビキタス言語(チーム全体で意図を共有するための言葉)を使うことで、意図の減衰を防止し、設計のいびつさを解消する
- 利用規約を読んでみる
- 厳密な言い回しで表記されているため、特化した名前の参考になる
- 違う名前に置き換えられないか検討する
- 既存の名前が意味範囲が十分に小さくなかったり、複数の意味を持ったりするため、意味をもっと狭くできないか、違和感がないかなどを検討する
- 疎結合高凝集になっているか点検する
注意点
- 会話に登場する重要な概念が、ソースコード上で名前もつけられず、雑多なロジックの中に埋没してしまうことがある
- 形容詞で区別が必要なときはクラス化のチャンス
名前設計の悪いケース
- 技術駆動命名
技術ベースでの命名をする
ex. コンピュータ技術由来(memory, cache, thread, register)、プログラミング技術由来(function, method, class, module)、型名由来(int, str, flag) - ロジック構造をなぞった名前
- 意図や目的がわかる名前に変更しよう
- 驚き最小の法則に従っていない名前
驚き最小の法則=「使う側が想像したとおりに、予想外な驚きが最小になるように設計する」 - データクラスに陥る名前
- 〜info, 〜Dataと命名されたクラスは、ロジックを実装してはいけないと印象付けてしまうため、低凝集に陥る
- DTD(Data Transfer Object) という、データ転送用途に使われる設計パターンの場合は例外
- 〜info, 〜Dataと命名されたクラスは、ロジックを実装してはいけないと印象付けてしまうため、低凝集に陥る
- クラスが巨大化する名前
- Manager, Processor, Controller などと名付けられたクラスはさまざまな責務のロジックが追加されがちになる
- Controller は受け取ったリクエストパラメータをほかのクラスへ渡す責務にとどめるべき(計算などはすべきでない)
- Manager, Processor, Controller などと名付けられたクラスはさまざまな責務のロジックが追加されがちになる
- 状況によって意味や扱いが異なる名前
コンテキストが異なるものは疎結合になるよう設計する - 連番命名
構造改善が困難になる - 名前的に居場所が不自然なメソッド
- 「動詞+目的語」のメソッド名は関心事が異なるメソッドになる傾向がある
- 可能な限り動詞1語で済む名前にする
- 「動詞+目的語」のメソッド → 目的語の概念を表現するクラスを作る。そのクラスに動詞一語のメソッドを追加する。
- 不適切な居場所のbooleanメソッド
- 「クラス名 is 状態」の形式に当てはまれば、適切なクラスに配置されている
- 「動詞+目的語」のメソッド名は関心事が異なるメソッドになる傾向がある
- 名前の省略
- 基本的に名前は省略しない
- スコープが極めて小さく意味混乱のリスクが小さければ省略してもよい
コメント
コメントのルール
- ロジック変更時、同時に必ずコメントも変更する
- 変更しなかった場合、ロジックと乖離した退化コメントが生じ、読み手が混乱するため
- ロジックの内容をなぞるだけのコメントをしない
- 可読性に貢献しない上、メンテナンスが大変になるため、退化コメントも発生しやすい
- 可読性の悪いロジックを補足説明するコメントをせず、ロジックの可読性を高める
- メンテナンスが大変になるため、退化コメントも発生しやすい
- ロジックの意図や仕様変更時の注意点をコメントする
- 保守や仕様変更時の助けになる
※ 「退化コメント」= 情報が古くなり実装を正しく説明しなくなったコメント
ドキュメントコメント
APIドキュメントの生成や、エディタ上でコメント内容のポップアップ表示ができる
ex. Java では Javadoc, C# では Documentaion comments, Ruby では YARD
Javadocの例
/** から */ までの範囲でコメントする
@param : 引数の説明
@throws : スローする例外の説明
@return : 戻り値の説明
コメントからAPIドキュメントを自動生成できる(IntelliJ IDEAなどを利用)
メソッド
- 必ず自身のクラスのインスタンス変数を使うこと
- 不変をベースに予期せぬ動作を防ぐ関数にする
- 尋ねるな、命じろ
呼び出されるメソッド側で複雑な制御をする - コマンド・クエリ分離(CQS)
- メソッドはコマンド(=変更)、または、クエリ(=問い合わせ)のどちらか一方だけを行うように設計する
- コマンド: 状態を変更する、クエリ: 状態を返す、モディファイア: コマンドとクエリを同時に行う
- 引数の扱い
- 引数は不変にする
- フラグ引数は使わない
- 可読性の低下が起こる
- ストラテジーパターンを使うなどの改善を
- nullを渡さない
- nullに意味を持たせない。未装備状態をnullではなくEquipment.EMPTYで表現するなど
- 出力引数は使わない
- 引数は可能な限り少なく
- 戻り値の扱い
- 型を使って戻り値の意図を表明する
- 独自の型を使って戻り値の意図を明確に表明する
- nullを返さない
- エラーは戻り値で返さない、例外をスローする
- 取り得る値の一部をエラーとして扱う=ダブルミーニング 避けるべき
- 型を使って戻り値の意図を表明する
モデリング
動作原理やしくみを簡単に理解・説明するだ「、物事の特徴や関係性を図式化したものをモデル、モデルをつくる活動をモデリングと呼ぶ。
モデリングをしないと、変更に弱く弊害が生じる。
モデリングの考え方とあるべき構造
システムは、目的達成のための手段。
モデルはシステムの構成要素であり、特定の目的達成のために最低限考慮が必要な要素を備えたもの。
-
一貫性のないモデルは、複数の目的のために無理矢理利用されており、モデリングしているようでモデリングしていない。
-
目的駆動で名前設計することが、適切に目的達成するモデルを設計することにつながる。
-
単一責任の原則 → 単一目的の原則
「クラスが果たす目的は、たったひとつに限定すべき」- 特定の目的に特化して設計することで、変更に強い高品質な構造になる
-
モデル≠クラス モデルは1個、または複数のクラスから構成される
- モデルはしくみを単純化したもの。モデルにもとづきクラスを設計し、コードを実装する。
- クラス、コードに落とし込む際に気づいたことは、モデルにフィードバックする
-
モデルを目的達成手段として解釈し、抽象化するとモデルに発展性が生まれる
- ex. サバ、サンマ、豚を栄養摂取手段として分類する
- ドメイン駆動設計では、「本質的課題を解決し、機能性の革新に貢献するモデル」を深いモデルと呼ぶ
モデルの見直し方
- そのモデルが達成しようとしている目的をすべて洗い出す
- 目的それぞれ特化したモデリングをし直す
- 目的駆動名前設計にもとづき、モデルに命名する
- モデルに目的外の要素が入り込んでいる場合、さらに見直す
リファクタリング
リファクタリングとは、外から見た挙動を変えずに、構造を整理すること。
挙動が変わっていないことを担保する手段としてユニットテストなどがある。
リファクタリングの流れ
- ネストを解消し、見通しを良くする
- 早期return
- 条件ん反転させる
- 意味のある単位にロジックをまとめる
- ex. 条件チェックと値の代入ロジックをそれぞれまとめる
- 条件を読みやすくする
- ex. ! hoge.isEnable() → hoge.isDisabled() へ
- ベタ書きロジックを目的を表すメソッドに置き換える
ユニットテスト
小さな機能単位で動作検証するテストをする。
テストフレームワークやテストコードを用い、メソッド単位で動作検証する手段を意味する。
リファクタリングにはユニットテストが必須!
まずは、コードの課題、あるべき構造を整理する
テストコードを用いたリファクタリングの流れ
- あるべき構造のひな型クラスをある程度つくる
- ひな型クラスに対してテストコードを書く
- テストを失敗させる
正しく失敗、成功できないも、テストコードまたはプロダクションコードに誤りがある可能性があるため - テストを成功させるための最低限のコードを書く
- ひな型クラス内部でリファクタリング対象のコードを呼び出す
- テストが成功するよう、あるべき構造へロジックを少しずつリファクタしていく
仕様が分からない場合
-
仕様分析方法1: 仕様化テスト
- メソッドの仕様を分析するための手法
- 適当な値を代入するテストを書き、どのような値が返るか確認し、メソッドがどのような挙動を示すかを明らかにする
-
仕様分析方法2: 試行リファクタリング
- 正式にリファクタリングするのではなく、ロジックの意味や構造を分析するためにお試しでリファクタリングする
- コードが整理されて見通しが良くなっていく
IDEのリファクタリング機能
例. IntelliJ IDEAの機能
- リネーム(名前の変更)
クラスやメソッド、変数の名前を一度に全て正確に変更する機能 - メソッド抽出
ロジックの一部をメソッドとして切り出す機能
リファクタリングで注意すべきこと
- 機能追加とリファクタリングを同時にやらない
- スモールステップで実施する
- コミットはどうリファクタしたか、違いがわかる単位にする
- 無駄な仕様は削除することも視野に
- リファクタ前には、無駄な仕様がないか、仕様の棚卸しをするのも一考
(コラム)
- Railsアプリのリファクタリング
RailsはActive Recordを中心としたMVCフレームワーク。Active Recordは、ControllerやViewと密結合しており、責務の異なる様々なロジックが集まる。
動的型付け言語のため、静的型付け言語に比べ、リファクタリングは高難易度となる。
Active Recordから責務の異なるロジックを引き剥がし、別のクラスとして分離する必要がある。
ただ、無理には引き剥がしたりせず、言語やフレームワークの特性を考慮した設計や移行手順を策定することが重要。
設計の意義と設計への向き合い方
設計とは、課題を効率的に解決するしくみづくり
この本の設計は、「変更容易性の向上を目的にした設計手法」
変更が困難で壊れやすいコード = レガシーコード
レガシーコードが蓄積している状態を「技術的負債」と呼ぶ → 開発生産性の低下
- バグを埋め込みやすくする
- 可読性が低下する
ソフトウェア価値のマトリックス
見える | 見えない | |
---|---|---|
プラスの価値 | 新機能 | アーキテクチャ |
マイナスの価値 | バグ | 技術的負債 |
理想的な設計と現状を比較することで、技術的負債が知覚可能になる
コードの良し悪しを判断する
コードの複雑さや、可読性などの一連の品質指標をコードメトリクス、またはソフトウェアメトリクスと呼ぶ
コードメトリクス
- 実行可能コードの行数
- 行数が多いと、多くのことをやりすぎている可能性あり
- Rubyのコード解析ライブラリRuboCop → コード行数の上限を定める
- 循環的複雑度
- コードの構造的な複雑さを示す指標
- 条件分岐やループ処理、ネストが複雑さ増大の原因
- 凝集度
- モジュール内における、データとロジックの関係性の強さを示す指標
- 粒度は、クラス、パッケージ、レイヤーなどがある
- 凝集度を表すメトリクスにLCOMがある
- 凝集度高い🙆♀️
- 結合度
- モジュール間の依存の度合いを表す指標
- 呼び出しクラスの数量を数えたり、クラス図を描画したりするだけでも、ある程度推量可能
- 結合度低い🙆♀️
- チャンク
- 人間の短期記憶は一度に4±1個の概念しか把握できない。この個数をマジカルナンバーと呼び、記憶個数の単位をチャンクと呼ぶ。
- クラス設計する際は、マジカルナンバー4を援用して脳にやさしい構造を心がける
コード分析ツール
- Code Climate Quality
GitHubと連携し、リポジトリ内のコードの品質を自動でスコアリングする - Understand
コード行数や複雑度、凝集度(LCOM)、結合度のほか、さまざまな観点のメトリクスを計測可能 - Visual Studio
Microsoft社のIDE(※VScodeではない)
実行可能コード行数や複雑度、結合度をファイルごとに分析。トータルの負債の度合いを示す保守容易性インデックスを算出可能。 - (補足)シンタックスハイライトを利用する
数値、引数、ローカル変数を警告色、finalインスタンス変数やクラス、interfaceは安全色とし、どこを改善すればよいか分かる