概要
参加していたインターンにてプルリクを出した際に大量のご指摘をいただいたので、きちんと「良いコードとは何か」について勉強をするべく、「良いコード・悪いコードで学ぶ設計入門」を読みました。本記事はそのまとめです。この記事に書いてある"悪しき構造"に少しでも心当たりがあれば実際に本を読むことをお勧めします。
第1章:悪しき構造の弊害を知覚する
悪しき構造の例
- 意味不明な命名(ex.
class Tmp) - 深すぎるネスト
- データクラス
- メソッドが実装されておらず、データのみを保持するクラスのこと。
第2章:設計の初歩
悪しき構造を撃退するためには以下のことを守る
- 変数名や関数名は意図が伝わるような名前にする
- 変数への再代入はせず、目的ごとの変数を用意する
- 変数への際代入を行うと、その変数に何の値が格納されているのか追いにくくなる
- 目的ごとにメソッドをまとめる
- 関連するデータとロジックをまとめる
- 関連するデータとロジックを一箇所にまとめることで、変更が必要になった際その箇所だけを変更すれば良くなる
第3章:カプセルの基本 -一つにまとめる-
- 関連するインスタンス変数とメソッドは一つにまとめる
関連するインスタンス変数とメソッドが一つのクラスにまとまって記述されていると、変更が必要になった際そのクラスだけを変更すればよくなる。一方、インスタンス変数とその変数に変更を加えるメソッドが別々に実装されていると、後々変更が必要になった際、変更漏れが生じやすい - 初期化はコンストラクタで必ず行う
- 引数なしのコンストラクタでインスタンスを生成し、後でインスタンス変数に代入をする方式だと未初期化状態でのインスタンス操作が行われやすい
- 必ずインスタンス変数で初期化を行う。また、初期化時点でバリデーションを行い、不正値が混入しないようにする。
- 変数の不変化を行い、再代入による不正値の混入などを防ぐ
- 値の渡し間違えを引数の型指定で防ぐ
- 引数の型指定を
intなど幅のあるものではなく、Moneyなどクラスで指定することで引数の渡し間違いを防ぐ - 変更を加えたい場合はインスタンスの再生成
- 引数の型指定を
第4章:不変の活用 -安定動作を構築する-
- 再代入を防ぐ
- 理由:再代入をすると、その変数の意味や値を追い辛くなる
- 対処法:ローカル・引数の変数をfinalなどで不変にする
- 副作用を防ぐ
- 主作用:関数が引数を受け取り、値を返すこと
- 副作用:主作用以外に、状態を変更すること
- 状態変更:
- インスタンス変数の変更
- グローバル変数の変更
- 参照型引数の変更
- ファイルの読み書きなどのIO動作
- 状態変更:
- 対策:関数の影響する範囲を限定する
- データ・状態は引数で受け取り、変更を加えない
- 値は戻り値として返す
- 不変と可変の使い分け
- デフォルトは不変。多少コードが冗長になってもコードの保守性などのメリットが勝る
- データの変更が多い場合や、扱うデータの容量が多く複数のインスタンスの生成がメモリ的に無駄が多い場合は可変を利用する。
- 可変の際には
- 状態を変化させる箇所を最小限に止める
- 適切なバリデーションなどを行う
第5章:バラバラなデータとロジックをカプセル化する実践手法
- バラバラなデータとロジックの例
- データクラスとstatic method
- プリミティブ型執着
- 引数や戻り値にintなどプリミティブ型を使用すること
- 初期化ロジックの分散
- 共通処理クラス(Util・Common)
- 出力引数の使用
- 出力引数とは出力を格納するための引数を指す(ex.
def shift(location, x): location.x += x) - 出力引数のクラスにその関数を実装すればカプセル化ができる
- 出力引数とは出力を格納するための引数を指す(ex.
- 引数が多すぎる
- アクセス連鎖
- インスタンス変数をドットで数珠繋ぎのように連続してアクセスすること(ex.
party.member.get(memberID).equipments.armar) - 問題点
- 本来設計すべき場所ではないのにこの連鎖の要素の中身を変更することができてしまう
- 連鎖の要素のうちどれか一つにでも変更が加わると直接影響を受ける
- 使用するモジュールの内部まで知りすぎている
→密にモジュールと結合している
→変更可用性が低い
- インスタンス変数をドットで数珠繋ぎのように連続してアクセスすること(ex.
- 「尋ねるな、命じろ(Tell, Don’t Ask)」に反する
- クラスやモジュールの内部の状態を尋ね、その返答に応じて処理を分岐するべきではない。なぜなら、そのクラスやモジュールの具体的な実装が外部に実装されてしまっており、ロジックの変更に弱くなるため。
- 処理が可能かなど内部状態の分岐ロジックはクラス内で行い、実際に行いたい処理のメソッドを呼び出すだけにする
第6章:関心の分離という考え方-分けて整理する-
- 関心とは「そのモジュールで達成したい目的」のことを指している
- 異なるデータを操作する場合はクラスを分けよう
- 現在のロジックの依存関係を調べる方法として影響スケッチを作成するのがよい。Javaを使用しているならJIGという可視化ツールが存在する。Pythonだと、pylintにセットでついてくるpyreverseというツールでER図などが作成できそう。
- 目的ごとに関心を分離しよう
- 例えば、
SellingPriceというクラスがあったとき、そのクラスにcalcSellingCommisionやcalcDeliveryPriceなどが含まれるのは適切ではない(どちらもSellingPriceから価格が決定するが)。なぜなら、どちらも販売価格を計算することが目的ではなく、販売手数料や輸送費を計算することが目的であるから。
- 例えば、
第7章:関心が混ざったコードを整理する実践的技法
- 単一責任の原則
- 一つのクラスや関数には一つの責任を負わせるという原則
- DRY(Don't Repeat Yourself)の原則の誤用に気を付ける
- コード自体が似通っていても、そのコードの目的(関心・責任)が異なっているのならばコードを纏めてはいけない。たとえば、通常時の金額と割引時の金額を同様の計算ロジックでまとめることは避けるべき。
- 継承の扱い方に気を付ける
- 継承を行うと、継承元クラスのコードを継承先のクラスは使用できるようになる。またオーバーライドすることによって、一部継承元クラスとは異なる処理を上書きすることができる。
- しかし、継承元クラスのコードを変更する際、自分を継承しているクラスのコード全てへの変更を考慮することが難しく、バグが混入しやすい。また、継承先クラスでオーバーライドしたメソッドが継承元クラスで意図せぬ挙動を生み出しやすく、バグが混入しやすい。
- 親クラスのメソッドを使用したい場合は継承ではなく、
元継承先クラスBが元継承元クラスAをインスタンス変数として保持し、インスタンス変数のメソッドを使用する。これにより、
- 関心が混ざったコードの特徴としては以下が挙げられる。
- global変数, public・privateメソッドが多い
- データクラス
- 巨大クラス・神クラス
第8章:条件分岐 -迷宮化した分岐処理を時ほぐす技法-
-
条件分岐(switch)が複雑なコードの場合
switchが至る所に実装されると変更耐性が低くなってしまう。
そこで以下のように実装を分離する。- インターフェースクラス
- (↑を継承する)サブクラス
- マッピング辞書(dict[case名, そのケースのインスタンス])
(配置する場所はインターフェースクラスか?)
既存コードでswitchを行っていたところではマッピング辞書を使用してサブクラスを入手する。caseで行っていた実装はサブクラスに実装され、呼び出される時はインターフェースクラスのメソッドとして呼び出す。
-
条件分岐(if)が複雑なコードの場合
会員クラスの分類など、複数の条件が絡む条件分岐の場合、以下のように実装する。
- 条件の格納・条件判定を行うインターフェースクラス(ex. RankPolicy)
- 具体的な条件の格納などを行うサブクラス(ex. GoldRankPolicy)
- 条件・ルールクラス(ex. 購入金額ルール)
これによって、ルール・ポリシーの変更に強い設計となる。
第9章:コレクション -ネストを解消する構造化技法-
リストやセットで実装された変数に関するメソッドが分散していると保守性が落ちる。これの対処法としてファーストクラスコレクションがある
ファーストクラスコレクションとは、以下の2要素で構成される。
- コレクション型(ListやSet)のインスタンス変数
- 完全性を保証するようにインスタンス変数を操作するメソッド
普通のクラスとの違いはインスタンス変数がコレクションかそうでないか。
コレクションの操作をクラスにまとめることでコレクションの中身が勝手に変わる可能性をなくせる。
第10章:設計の健全性を損なう様々な悪魔たち
- 不要なコードは削除しよう
- YAGNI原則を守ろう
- YAGNI原則とは、You aren’t going to need itの略で、実際に必要になった時だけ実装しなさいという原則。先取りして実装されるコードの大半はいらないことが多いため。
- マジックナンバーは適切な名前の定数として定義しよう
- 不用意なグローバル変数化は避けよう(同様に巨大なデータクラスもグローバル変数と同様の性質を保つため避けよう)
- nullの入力・出力・設定は避けよう
- 未初期化である状態を表したい場合は
Emptyという状態を作成し、これを代入するようにする
- 未初期化である状態を表したい場合は
- 例外は握りつぶさない
- 例外を握りつぶすと予期せぬエラーを呼び起こすだけでなく、バグの場所がわからずバグ修正に時間がかかるようになる
- 不用意なメタプログラミングは避けよう
- 動的にクラス名などをつけることは静的な解析ツールの使用を困難にする
- 技術駆動パッケージングは避け、目的駆動パッケージングをしよう
- 技術駆動パッケージングとは、使用技術的に似ているコードを一つのフォルダやパッケージにまとめるパッケージング方法。目的がばらけるため、異なるフォルダ間での依存関係が生じてしまう。
- サンプルコードのコピペはやめよう
- サンプルコードは動くだけ。自分のコードに最適ではない。
- 目的別に使用するフレームワークを分けよう
- 本書籍で紹介されているフレームワークは「変更可用性を高める」ものであるため、以下のようなケースでは実装コストが高くつく。
- 使用があまり変更されない
- プロトタイプ
- 寿命間近のサービス
- 本書籍で紹介されているフレームワークは「変更可用性を高める」ものであるため、以下のようなケースでは実装コストが高くつく。
第11章:名前設計 -あるべき構造を見破る名前-
- モジュールやクラスなどの名前は目的駆動名前設計をすべき。目的駆動名前設計とは、その対象の目的(関心)に基づいて名前を設計することを指す。存在駆動名前設計とは、目的駆動とは対照的に、商品やユーザなど、モデルの関係の中に存在する概念の名称をそのまま用いた命名方法を指し、避けるべきとされる。
- 目的駆動名前設計で重要な点は以下の通り。
- 可能かなぎり具体的で、意味が狭い、目的に即した名前を選ぶ
- 例:商品 → 在庫品・購入品など、その商品の達成したい目的に即した具体的で意味の狭い名前をつける
- その命名対象の役割を考える
- 声に出して話してみる(ラバーダッキング)
- 利用規約を読んでみる
- 利用規約はサービスに関して厳密に言語化されており、クラスやメソッドとして実装すべき概念を見つけやすい
- 違う名前に置き換えられないか検討する
- 関心が分離されていないか確認する
- 会話では登場するがコードに表れていない概念はないか確認する
- 形容詞で区別しているクラスはクラス分けできないか検討する
- 可能かなぎり具体的で、意味が狭い、目的に即した名前を選ぶ
名前設計時の注意点
- コードの更新時にも関心が分離していないか検討する
- 元々は個人用クラスで良かったところに、法人用に必要な情報を詰め込んでしまうと関心が分離してしまう
- アンカリング効果に気をつけよう
- アンカリング効果とは最初に提示されたものがその後の判断の基準となり判断を歪める効果のこと。
- 対処法としては以下の質問例のようにそのクラスの「目的」「存在意義」を説明できるか検証する方法が挙げられる。
- このクラスの正体は何か?別概念である可能性はないか?
- このクラスの目的を教えてください。何をもって目的達成としますか?
- 重要な概念はクラスやメソッドの名前として記述して登場させよう
- ドキュメントやコードにも記述されていない名前のついていない機能を認識するのは困難で保守性を著しく損ねる。その機能が実装されていることが認識できない。
- ジョシュアツリーの法則:名前を知らないと知覚できない
- 技術駆動やロジック駆動の命名は避けよう
-
ManageCacheやisnotGoldandnotSilverなどは避ける -
isnotGoldandnotSilverならisNormalCustomerのようにその関数の目的で命名する
-
- 驚き最小の法則を意識しよう
- メソッドやクラスの実態と名前の一貫性をとり、名前からメソッドやクラスの実態を推測できるようにする
第12章:コメント -保守と変更の正確性を高める書き方-
- 退化コメントは削除しよう
- ロジックの内容をなぞるようなコメントは不要。ロジックが変わった途端退化コメントとなるため、保守性の低下を招く。
- わかりにくい命名の補助としてコメントを使用するのはやめよう。命名を工夫しよう。
- その関数の目的やどのようにしたら安全に変更ができるかなどのコメントはあり。
第13章:メソッド(関数) -良きクラスには良きメソッドあり-
良いメソッドは以下の項目を満たす。
- 必ず自分のインスタンス変数を使用する
- 不変をベースに予期せぬ変更を防ぐ
- 尋ねるな、命じろ(Tell, Don't Ask)
- 他のクラスの状態を確認したり、その状態に応じて処理を変更したりしないこと
- コマンド・クエリ分離(CQS)
- 状態の取得や状態の変更を行うメソッドを一つに纏めず分離する
- 引数
- 引数は不変
- フラグ引数は使用しない
- 出力引数は使用しない
- nullは渡さない
- 引数は限りなく少なく
- 戻り値
- 型を指定して意図をわかりやすく
- nullを返さない
- エラーは戻り値でなく、例外を送出する
第14章:モデリング -クラス設計の土台-
システムを構築する前に、クラス図を活用し、モデリングを行う。モデルはシステムに必要な最小限な機能を備えたコンポーネントで構成される。モデルをコードに落とし込み、詳細な設計を行う過程で問題が生じたらまたモデルを更新しブラッシュアップしていく。
システムを構築する前に、ユースケース図を活用し、システムの関係者を洗い出す。これにより、システム(使用されるもの)とアクター(使用するもの)が分けて考えやすくなる。その後にクラス図を作成するとより具体的なクラス図が作成できる。
第15章:リファクタリング -既存コードを成長に導く技-
リファクタリングの流れ
- ネストの解消
- 意味あるロジックの単位でまとめる
- if文などの条件を読みやすくする
- 論理否定を使わない(ex.
if (!customer.isEnable())->if custormer.isDisable())
- 論理否定を使わない(ex.
- ベタ書きロジックを目的を表すメソッド名のメソッドに置き換える
安全にリファクタリングする方法
- コードの課題を整理する
- テストコードを用いたリファクタリングの流れ
あやふやな使用を理解するための分析方法
- 仕様化テスト
- 適当な値を代入し、出力を確認する
- 試行リファクタリング
- メインコードに対してではなく、メインコードからチェックアウトしたコードに対して行うリファクタリング
- コードの可読性向上によるコード理解の深化
- あるべき構造が見えてくるため、本番リファクタリングでの構造決定に役立つ
リファクタリングを行う際の注意点
- 機能追加とリファクタリングを同時に行わない
- リファクタリングは小規模から行う
- 無駄な仕様は削除を検討
第16章:設計の意義と設計への向き合い方
- 設計をしないとかえって開発速度が低下する
- バグが多くなる
- 変更可用性が低く、変更に時間がかかる
- 可読性が低い
- レガシーコードはエンジニアの成長を妨げる
- 良いコードを見られず、悪いところを真似てしまう
- 良い設計を体験することができない
- 時間ばかりが取られる
- 設計の品質を図る指標
-
開発生産性←これは比較が難しい
-
コードメトリクス・ソフトウェアメトリクス
コードの複雑さや品質を図る指標。
- 実行可能コード数(コメントをのぞいたコード数)
- メソッド:10行以内
- クラス:100行以内
- 循環的複雑度:ifやfor文の数
- 10以下:非常に健全
- 30以下:構造的問題あり
- チャンク
- 人間は4+1までしか短期記憶できない
- コードの結合を4までに絞ることで人間に優しい設計となる
- 実行可能コード数(コメントをのぞいたコード数)
-
- 設計と費用対効果
- 競合優位性を出すような価値に対しては設計に時間をかける効果がある。
- パレートの法則: 価値の8割は2割のコアから生まれる。その2割に対して労力を使う。
第17章:設計を妨げる開発の進め方との戦い
- コミュニケーションをとろう
- 心理的安全性を担保しよう。間違っても大丈夫という雰囲気を作ろう。
- コミュニケーションが取れていないと設計がうまく行っていても、重複する関数などが実装されやすい
まとめ
本書籍では「変更可用性」に重きを置いていました。変更可用性を高めるために特に重要な概念である「単一責任の原則」と「DRYの原則」を実現する手段として「カプセル化」があり、それを実現するための手法がたくさん紹介されていました。本書籍に記されている手法の実践時にも、「単一責任の原則」と「DRYの原則」を特に意識すると良いかもしれません。
おわりに
今回は書籍をざっくりまとめました。
自分見解を含んだアウトプットですので、間違いなどあればご指摘いただけると幸いです。