はじめに
設計について学び、学んだことをまとめました
1章 悪しき構造の弊害を知覚する
設計を蔑ろにすると以下の弊害が起こる
- コードを読み解くのに時間がかかる
- バグを埋め込みやすくなる
- 悪しき構造がさらに悪しき構造を誘発する
1.1 意味不明な命名
型名を表すInt、メモリ制御を表すMemoryやFlagなど、プログラミング用語やコンピュータ用語に基づいた名前、技術ベースでの命名を"技術駆動命名"と呼ぶ
クラスやメソッドに対し番号付で命名するのを連番命名と呼ぶ
これらは意図が全く読み取れない、悪しき手法
1.2 理解を困難にする条件分岐のネスト
入れ子構造になっている状態をネスト、またはネストしているという
ネストはコードの見通しが悪くなる
1.3 さまざまな悪魔を招きやすいデータクラス
データを保持するだけのクラスをデータクラスと呼ぶ
関連するデータやロジック同士が分散し、バラバラになっているのを低凝集と言う
弊害は以下になる
- 重複コード
- 修正漏れ
- 可読性低下
- 見初期化状態
初期化しないと使い物にならないクラス、または見初期化状態が発生しうるクラスを、アンチパターン生焼けオブジェクトと呼ぶ - 不正値の混入
注文数がマイナスになっている、ゲームにおいてヒットポイントの値が最大を超えてしまっている
2章 設計の初歩
2.1 省略せずに意図が伝わる名前を設計する
意図がわかる変数名に改善する
2.2 変数を使い回さない、目的ごとの変数を用意する
変数に再度値を代入をすることを再代入と呼ぶ
2.3 ベタ書きせず、意味のあるまとまりでメソッド化
意味のあるまとまりでロジックをまとめメソッド(関数)として実装する
2.4 関係し合うデータとロジックをクラスにまとめる
3章 クラス設計 -すべてにつながる設計の基盤-
3.1 クラス単体で正常に動作するように設計する
クラスの構成要素は次の2つ
- インスタンス変数
- メソッド
良いクラスの構成要素は
- インスタンス変数
- インスタンス変数を不正状態から防御し、正常に操作するメソッド
- メソッドは必ずインスタンス変数を使用する
3.2 成熟したクラスへ成長させる設計術
処理の対象外となる条件をメソッドの先頭に定義する方法をガード節と言う
計算ロジックをデータ保持側に寄せる
不変で思わぬ動作を防ぐ(再代入できなくする)
変更したい場合は新しいインスタンスを作成する
メソッド引数やローカル変数も不変にする
「値の渡し間違い」を型で防止する(int型やString型といった、プログラミング言語が標準で用意している基本データ型をプリミティブ型と呼ぶ)
現実の営みにはないメソッドを追加しない
クラス設計とは、インスタンス変数を不正状態に陥らせないための仕組みづくり
3.4 プログラム構造の問題解決に役立つ設計パターン
設計パターン | 効果 |
---|---|
完全コンストラクタ | 不正状態から防護する |
値オブジェクト | 特定の値に関するロジックを高凝集にする |
ストラテジ | 条件分岐を削減し、ロジックを単純化する |
ポリシー | 条件分岐を単純化したり、カスタマイズできるようにする |
ファーストクラスコレクション | 値オブジェクトの亜種で、コレクションに関するロジックを高凝集にする |
スプラウトクラス | 既存のロジックを変更せずに安全に新機能を追加する |
4章 不変の活用 -安定動作を構築する-
変数の値を変更するなど状態変更できることを可変(ミュータブル)と呼び、状態変更できないことを不変(イミュータブル)と呼ぶ
4.1 再代入
変数に再度値を代入することを再代入、または破壊的代入と呼ぶ
- 不変にして再代入を防ぐ
- 引数も不変にする
4.2 可変がもたらす意図せぬ影響
-
可変インスタンスの使い回し
同じインスタンスを使用している場合、値の変更をしたら他の箇所も値が変更される -
関数による可変インスタンスの操作
別のスレッドで使われる可能性がある -
副作用のデメリット
副作用とは、関数が引数を受け取り、戻り値を返す以外に、外部の状態(変数など)を変更すること
関数(メソッド)には、主作用と副作用がある
- 主作用:関数(メソッド)が引数を受け取り、値を返すこと
- 副作用:主作用以外に状態変更すること
状態変更とは、関数の外にある状態の変更のこと
- インスタンス変数の変更
- グローバル変数の変更
- 引数の変更
- ファイルの読み書きなどのI/O操作
関数の影響範囲を限定する
関数が次の項目を満たすことを前提に設計する
- データ、つまり状態を引数で受け取る
- 状態を変更しない
- 値は関数の戻り値として返す
不変にして予期せぬ動作を防ぐ
- 可変インスタンスを使用すると、仕様変更時に意図せず副作用のある関数が作り込まれてしまい、予期しない動作を招いてしまう可能性がある
4.3 不変と可変の取り扱い方針
デフォルトは不変に
不変にすると以下のメリットが得られる
- 変数の意味が変化しなくなるので、混乱が抑えられる
- 挙動が安定し、結果を予測しやすくなる
- コードの影響範囲が限定的になり、保守が容易になる
どんな時に可変にして良いか
- パフォーマンスに問題が生じる場合
- スコープが局所的なケース
正しく状態変更するメソッドを設計する
- 可変にする場合は、正しく状態変更できる作りにする
- 状態変更を発生させるメソッドをミューテーターと呼ぶ
コード外とのやりとりは局所化する
5章 低凝集 -バラバラになったモノたち-
5.1 staticメソッドの誤用
-
staticメソッドはインスタンス変数が使えない
staticメソッドはインスタンス変数を使えない、staticメソッドを持ち出した時点で、データとデータを操作するロジックが乖離し、どうしても低凝集にならざるを得ない -
インスタンス変数を使う構造に作り方
インスタンス変数と、そのインスタンス変数を用いるロジックを同じクラス内に閉じ込めた構造が高凝集 -
インスタンスメソッドのフリしたstaticメソッドに注意
staticキーワードがついていないだけで、staticメソッドと同じ問題を抱えているインスタンスメソッドもある -
どうしてstaticメソッドが使われてしまうのか
staticメソッドが使われる背景には、C言語などの手続き型言語の考え方が影響している -
どう言う時にstaticメソッドを使えば良いのか
凝集度に影響がない場合に、staticメソッドを使える
5.2 初期化ロジックの分散
十分にクラス設計しても、初期化ロジックがあちこちに分散して低凝集になってしまう場合もある
-
privateコンストラクタ+ファクトリメソッドで目的別初期化
初期化ロジックの分散を防ぐには、コンストラクタをprivateにし、代わりに目的別のファクトリメソッドを用意する -
生成ロジックが増えすぎたらファクトリクラスを検討すること
生成ロジックがあまりにも長大になるようなら、生成専門のファクトリクラスとして分離することも検討
5.3 共通処理クラス(Common・Util)
staticメソッドのもう一つの形として頻繁に見られるのが、共通処理の置き場所として用意されたクラス(共通処理クラス)。Common、Utilなどと名付けられることが多い
-
さまざまなロジックが雑多に置かれがち
原因の一つは、CommonやUtilといった「共通」を匂わせる名前で、「共通利用したいロジックはCommonクラスにおけば良いんだ」と読み手に感じさせてしまう可能性がとても高い。根本的な原因は、共通化や再利用性に関して理解が不足していることで、再利用性は、高凝集な設計にすることで高まる。 -
オブジェクト指向設計の基本に立ち返ろう
共通処理クラスを安易に作らないようにしましょう、そしてオブジェクト指向設計の基本に基づいて設計しよう -
横断的関心事
さまざまなユースケースに広く横断する事柄を、横断的関心事と呼ぶ。代表的なのは以下
・ログ出力
・エラー検出
・デバッグ
・例外処理
・キャッシュ
・同期処理
・分散処理
5.4 結果を返すために引数を使わないこと
引数の扱いを誤ると低凝集に陥りやすい
5.5 多すぎる引数
引数が多すぎるメソッドは、低凝集に陥るよくない構造
-
プリミティブ型執着
boolean、int、float、double、Stringといった、プログラミング言語が標準で用意している基本データ型をプリミティブ型と呼ぶ
プログラミング初心者やプリミティブ型を中心に長年コードを書いてきたプログラマーには、クラス設計の習慣がないのでプリミティブ型執着に陥りがち -
意味のある単位ごとにクラス化する
引数が多すぎる事態に陥らないためには、概念的に意味のあるクラスを作ることが肝要
5.6 メソッドチェイン
「.」(ドット)で数珠繋ぎにして、戻り値の要素に次々にアクセスする書き方をメソッドチェインと呼ぶ
- 尋ねるな、命じろ
ソフトウェア設計には、尋ねるな、命じろ(Tell,Don'tAsk.)という有名な格言がある、他のオブジェクトの内部状態(つまり変数)を尋ねたり、その状態に応じて呼び出し側が判断したりするのではなく、呼び出し側はただメソッドで命ずるだけで、命令された側で適切な判断や制御をするよう設計する
6章 条件分岐 -迷宮化した分岐処理を解きほぐす技法-
条件分岐は、条件に応じて処理内容を切り替えるための、プログラミングの基本制御。条件分岐のおかげで複雑な判断を高速かつ正確に実行でき、サービス利用者に恩恵をもたらしている。一方で、条件分岐をずさんに扱うと、悪魔となって開発者を苦しめることになる。条件が複雑になると見通しが悪くなり、理解が困難になる。理解が困難だとデバックや仕様変更に時間がかかり、分岐ロジックを正確に理解せずに仕様変更すれば、バグを生んでしまう。
6.1 条件分岐のネストによる可読性低下
ネストをしていくとコードの見通しが悪くなっていく。
どこからどこまでがif文の処理ブロックなのか、読み解くのが難しくなる。
-
早期returnでネスト解消
早期returnとは、条件を満たしていない場合に、直ちにreturnで抜けてしまうという手法 -
見通しを悪くするelse句も早期returnで解決
else句も、見通しを悪化させる原因の一つ
6.2 switch文の重複
switch文は悪魔を非常に呼び寄せやすい制御構文で、バグを埋め込んだり、可読性を低下させたりしてしまう
-
即座にswitch文を書いてしまう
種類ごとに処理を切り替えるロジックを実装する場合、switch文が使われることが多い -
同じ条件式のswitch文が複数書かれていく
-
仕様変更時の修正漏れ(case文追加漏れ)
-
爆発的に増殖するswitch文の重複
切り替え対象の分だけメソッドが増え、増えたメソッドそれぞれでswitch文を書かなければならなくなる。switch-case文が爆発的に増えてしまう。 -
条件分岐を一箇所にまとめる
switch文の重複コードを解消するには、単一責任選択の原則の考え方が重要。端的に言うと、同じ条件式の条件分岐を複数かかず、一箇所にまとめよう、と言う原則 -
よりスマートにswitch文重複を解消するinterface
単一責任選択の原則に基づきswitch文は一箇所に纏まりましたが、切り替えたいものが増えた場合ロジックは膨れ上がる。クラスが巨大になると、データやロジックの関係性がわかりにくくなり、保守や変更が難しいコードになってくる。
この課題に役立つのがinterfaceで、Javaなどのオブジェクト指向言語特有の仕組みで昨日の切り替えや差し替えを容易にする。interfaceを使うと、分岐ロジックを書かずに分岐することが可能になる。 -
interfaceをswitch文重複に応用(ストラテジパターン)
種類ごとに切り替えたい機能をinterfaceのメソッドとして定義する。interfaceを用いて処理を一斉に切り替える設計をストラテジーパターンと呼ぶ
未実装のメソッドをコンパイラが叱ってくれる
丁寧に値オブジェクト化する
6.3 条件分岐の重複とネスト
interfaceはswitch文の重複解消以外にも、多重にネストし複雑化した分岐の解消にも役立つ
- ポリシーパターンで条件を集約する
条件の部品化、部品化した条件を組み替えてのカスタマイズを可能にする
6.4 型チェックで分岐しないこと
interfaceは条件分岐の削減に役立つが、使っても条件分岐が減らない、良くならないやり方がある
interfaceofを使った同じ条件分岐を何度もかき重複コードが増える、このようなロジックはリスコフの置換原則と呼ばれソフトウェア原則に違反している
interfaceの意義を十分に理解していない場合、この手のロジックに陥る
6.5 interfaceの使いこなしが中級者への第一歩
interfaceをうまく駆使すると条件分岐が大幅に減り、コードがシンプルになる。interfaceを使いこなせるかが、設計スキルの分水嶺といっても過言ではない
6.6 フラグ引数
メソッドの機能を切り替えるboolean型引数をフラグ引数と呼ぶ、フラグ引数つきのメソッドは、何が起こるか読み手に想像を難しくさせるため可読性が低下し、開発生産性が低下する
-
メソッドを分離する
フラグ引数付きメソッドは、内部に複数の機能を持ち、フラグで切り替えている構造。メソッドは単機能になるように設計しよう。フラグ引数付きのメソッドは、機能毎に分離すること -
切り替え機構をストラテジパターンで実現する
7章 コレクション -ネストを解消する構造化技法-
7.1 わざわざ事前でコレクション処理を実装してしまう
7.2 ループ処理中の条件分岐ネスト
-
早期continueで条件分岐のネストを解消する
ループ処理中の条件分岐ネストは、早期countinueで解決可能
countinueは実行中の残り処理をスキップし、次のループ処理へ移行する制御構文。早期returnは「条件を満たさない場合にreturnで抜ける」と言う手法 -
早期breakもネスト解消に役立つ
ループ処理中の条件分岐ネストは早期continueで解決可能。continueは実行中の残り処理をスキップし、ルギのループ処理並行する制御構文で、早期returnは「条件を満たさない場合にcontinueで次のループ処理に移行する」書き方に変える -
早期breakもネスト解消に役立つ
ループ処理の制御構文には、continue以外にもbreakがあり、breakは処理中断し、ループから抜け出す制御構文。早期continueと同じ発想で、早期breakによりロジックの単純化が可能なパターンがある
7.3 低凝集なコレクション処理
- コレクション処理をカプセル化する
コレクションの低凝集を解決するのがファーストクラスコレクション、ファーストクラスコレクションとは、コレクションに関連するロジックをカプセル化する設計パターン
クラスには以下の2つが備わっている必要がある
・インスタンス変数
・インスタンス変数を不正状態から防御し、正常に操作するメソッド
ファーストクラスコレクションは、この考え方の応用で、次の要素を備える
・コレクション型インスタンス変数
・コレクション型インスタンス変数を不正状態から防御し、正常に操作するメソッド
外部へ渡す場合はコレクションを変更できなくする
8章 密結合 -絡まって解きほぐせない構造-
結合度とは、「モジュール間の、依存の度合いを表す指標」で、凝集度と同様に、モジュールの粒度をクラスとする
あるクラスが、他の多くのクラスに依存している構造を密結合と呼ぶ
結合度の低い、疎結合な構造へ改善すると、コードの変更が楽になる
8.1 密結合と責務
-
ロジックの置き場所がチグハグ
各ロジックの置き場所がチグハグで一部のクラスに処理が集中していたり、一方別のクラスではなにっも処理を持っていなかったり、他のクラスの一部のメソッドを無理やり都合よく流用したりする。このようなクラスが責務が考慮されていないクラスという -
単一責任の原則
「クラスが担う責任は、たった一つに限定すべき」とする設計原則 -
責務が単一になるようクラスを設計する
関心毎がそれぞれ分離、独立している構造を疎結合と呼び、密結合とは逆の構造である。疎結合な設計を目指すべきである -
DRY原則の誤用
「繰り返しを避けよ」一部では、DRY原則が「コードの重複を許すな」といった解釈で広まっているが
全ての知識はシステム内において、単一、かつ明確な、そして信頼できる表現になっていなければならない。とされている
同じようなロジック、にているロジックであっても、概念が違えばDRYにすべきではない、概念的に異なるもの同士を無理にDRYにすると密結合になり、単一責任原則を遵守できなくなる
8.2 密結合の各種事例と対処方法
- 継承に絡む密結合
継承はかなり注意して扱わないと、すぐに密結合に陥る
継承はよっぽど注意して扱わないと危険、継承は推奨しない
・スーパークラス依存
サブクラスは、スーパークラスの構造をいちいち気にしなければならない。スーパークラスの動向によっぽど注意していないと、バグが発生する
・継承より委譲
スーパークラス依存による密結合を避けるため、継承より委譲が推奨される。委譲とはコンポジション構造にすること
・継承による悪しき共通化
継承を使うとサブクラスがスーパークラスのロジックを使えるようになるため、スーパークラスが共通ロジックの置き場所として利用されがち
- なんでもpublicで密結合
publicやprivateなどアクセス修飾子を付与することで、クラスやメソッドの可視性を制御できる、しかし、なんでもpublicにすると密結合になる
Javaのアクセス修飾子
アクセス修飾子 | 説明 |
---|---|
public | 全てのクラスからアクセス可能 |
protected | 同じクラス、または継承クラスかアクセス可能 |
なし | 同じパッケージからのみアクセス可能。package privateと呼ぶ |
private | 同じクラスからのみアクセス可能 |
-
privateメソッドだらけ
ソフトウェアの機能が拡充されていくと、クラスはどんどん大きくなってくる
いびつな依存を排除するため、他のクラスから呼び出されないようprivateメソッドとして実装されることが多いが、privateメソッドが多いクラスは、単一責任ではなく、多くの責務を持ってしまう -
高凝集の誤解からくる密結合
高凝集を意図して強く関係していそうなロジックを一箇所にまとめ上げようとしたものの、結果として密結合に陥っているケースはかなり多い。それぞれの概念は分離し、疎結合にしなければならない -
スマートUI
表示関連のクラスの中に、表示以外の責務のロジックが実装されている構造をスマートUIと呼ぶ -
巨大クラス
データクラスがさらに巨大化したのが巨大データクラス -
トランザクションスクリプトパターン
メソッドないに一連の処理手順がダラダラと長く書き連ねられている構造をトランザクションスクリプトパターンと呼ぶ -
神クラス
トランザクションスクリプトパターンがより重症化すると神クラスになる
神クラスとは、1クラス内に何千何万行ものロジックをもち、あらゆる責務のロジックが乱雑に絡み合うように描き殴られているクラス -
密結合クラスの対処法
巨大な密結合クラスは責務毎にクラスを分割する
9章 設計の健全性をそこなうさまざまな悪魔たち
9.1 デッドコード
どんな条件であっても決して実行されないコードをデッドコード、または到達不能コードと呼ぶ
デッドコードは、発見次第すぐに削除するべき
YAGNI原則
YAGNIと呼ばれるソフトウェア原則がある、これは「You aren't going to need it.」の略で、訳すと「必要ないでしょう」になる。実際に必要になった時のみ実装せよ、という方針
9.3 マジックナンバー
ロジックないに直接書き込んでいる意図不明な数字をマジックナンバーと呼ぶ。マジックナンバーは実装者本人にしかほとんど意図を理解できない
9.4 文字列型執着
9.5 グローバル変数
どこからでもアクセス可能な変数をグローバル変数と呼ぶグローバル変数を使っていなくても、グローバル変数と同質のものを知らず知らずのうちに使っていることが多い
- 影響範囲を最小化するよう設計すること
グローバル変数(および巨大データクラス)は、影響範囲が広すぎる。多くの箇所から呼び出し可能な構造/呼び出しやすい構造である
影響範囲が最小化するように設計しよう、無関係なロジックからはアクセスができないように設計すると、呼び出し箇所が少なく、局所化されているほど、ロジックの理解が容易になり、正しく動作するロジックを実装しやすくなる
9.6 null問題
nullが入り込む前提でロジックを組むと、至る所でnullチェックをしなければならなくなる、nullチェックだらけでコードの見通しが悪くなり、nullチェックが漏れるとバグになる
そもそもnullとは、未初期化状態のメモリ領域へのアクセスは、制御上トラブルの原因になる。こうした事態を避けるためにnullが開発され、nullは最低限メモリアクセストラブルを防止するための仕組みであって、null自体は無効な扱い
-
nullを返さない、渡さない
null例外によるトラブルやnullチェックを避けるために、そもそもnullを取り扱わない設計にすることが大事です。具体的には次を満たす設計にする
・nullを返さない
・nullを渡さない
nullを返さない設計とは、メソッドの戻り値としてnullをreturnしないことで、nullを渡さない設計とは、nullを変数に代入しないこと -
null安全
null安全とは、nullが原因のエラーを発生させない仕組み。一部のプログラミング言語には、null安全の仕様を持つものがある。null安全を実現する機能の一つに、null非許容型があり、null非許容型とは、nullを保持できない型である
9.7 例外の握りつぶし
例外をキャッチしても、なんの処理もしない、例外の握り潰しと呼ばれる、極めて邪悪なロジックである
-
原因分析困難に陥り開発者を疲弊させる
例外を握り潰す事の問題は、エラーが起こっても、外から検知する術がなくなってしまうこと
内部的にはデータが壊れるなど、不正状態に陥っているものの、外からはなんの問題もなく動いているように見えてしまいます、壊れたデータに基づき、さらに別の壊れたデータが連鎖的に作られてしまう可能性すらある -
問題検出時にけたたましく叫ばせる
こうした恐ろしい事態を避けるためにも、不正状態に対して寛容になるべきではない
状態が不正なまま通常処理を続行するのは、爆弾の導火線に火がついているのも知らずに、爆弾を持ってウロウロと歩き回るのと変わらない
以上にはすぐに気づく仕組みにし、例外をキャッチした時には通知や記録、場合によってはリカバリ処理を実行すること
9.8 設計秩序を破壊するメタプログラミング
プログラム実行時に、そのプログラム構造事態を制御するプログラミングをメタプログラミングと呼ぶ、メタプログラミングの技術の一つとして、Javaではクラス構造を読み書きできる構造、リフレクションが用意されている
-
リフレクションによるクラス構造および値の変更
リフレクションを濫用すると不正状態から防護する設計や、影響範囲を閉じ込める設計が全く意味をなさなくなってしまう -
型の強みを活かせなくなるハードコード
Javaに代表される静的型付け言語は、静的解析により正確にコード分析可能なのが強みだが、メタプログラミングはそうした型の強みを打ち消してしまう -
デメリットを理解し用途を限定すること
システム解析用途に限定する、メタプログラミングの利用箇所を極めてスコープの狭い特定箇所に閉じ込めるなど、リスクが生じない工夫が必要
9.9 技術駆動パッケージング
設計パターンなど、構造的に似ているもの同士でフォルダを分け、パッケージ分けするのを技術駆動パッケージングと呼ぶ
10章 名前設計 -あるべき構造を見破る名前-
10.1 悪魔を呼び寄せる名前
-
関心の分離
密結合を解消し、疎結合高凝集にするためには関心の分離が重要、関心の分離とは「関心毎、つまりユースケースや目的、役割毎に分離する」というソフトウェア工学における考え方 -
大雑把で不明瞭な名前
目的不明なクラスを目的不明オブジェクトと呼称し、これらはあっという間に巨大化する
10.2 名前を設計する-目的駆動名前設計
プログラミングにおける名前の役割は、可読性を高めることだけではない
関心の分離を意識し、ビジネス目的に沿った名前を付与することは、疎結合高凝集を実現する上で重要、設計上大きな意味があるため「名前を設計する」という
目的駆動名前設計は、目的をベースに名前を設計する
以下が重要なポイント
・可能な限り具体的で、意味範囲が狭い、特化した名前を選ぶ
・存在ベースではなく、目的ベースで名前を考える
・どんな関心毎があるか分析する
・声に出して話してみる
・利用規約を読んでみる
・違う名前に置き換えられないか検討する
・疎結合高凝集担っているか点検する
-
可能な限り具体的で、意味範囲が狭い、特化した名前を選ぶ
顧客向けプロダクト開発における目的は、「会社の事業的にどういう目的を達成したいのか」というビジネス目的になる
ビジネス目的に特化することで以下の効果が生まれる
・名前とは無関係なロジックを排除しやすくなる
・クラスが小さくなる
・関係するクラスの個数が少なくなる。結合度が低減する
・関係クラス個数が少ないので、仕様変更時に考慮を要する影響範囲が小さくて済む
・目的に特化した名前なので、どこを変更すればいいかすぐ探し出せる
・開発生産性が向上する -
どんなビジネス目的があるか分析する
ビジネス目的に特化した命名をするには、どんなビジネス目的があるか網羅する必要がある -
声に出して話してみる
人が脳内で考えていることは意外なほどぼんやりしていてはっきりとしないため声に出して話してみると原因に気づきやすい -
利用規約を読んでみる
利用規約には、サービスの取扱いやルールが極めて厳密な言い回しで書かれており、特化した名前の参考になる -
違う名前に置き換えられないか検討する
せっかく選んだ名前の意味範囲が十分に小さくなかったり、複数の意味を持ったりしてしまっている可能性が多々ある、違う名前に置き換えてみて、意味をもっと狭くできないか、違和感がないかなどを検討する -
疎結合高凝集担っているか点検する
目的に特化した名前を選ぶと、目的以外のロジックを寄せ付けにくくする、目的だけのロジックが集まりやすくなり、高凝集になる。目的以外のロジックが混入しそうなら、名前を見直す
10.3 設計時の注意すべきリスク
-
名前無頓着になるな
自動駆動名前設計の考え方は、「名前に注意を払い、名前とロジックを対応づけること」を前提にしている。したがって、名前に無頓着だと全てが瓦解する
チーム開発においては、命名が重要であり、名前とロジックが対応する前提であること、名前がプログラム構造を大きく左右することをチーム内で約束するべき -
仕様変更時の「意味範囲の変化」に警戒
開発中の度重なる仕様変更に伴い、開発の文脈で言葉が意味するところはどんどん変化していくことがある。そのため、名前設計には見直しが必要 -
形容詞で区別が必要なときはクラス化のチャンス
利害の見分けが難しいコードを、口頭でひたすら形容詞をつけて同僚に説明する状況が、システム開発では頻繁に生じる。形容詞で表現している場合、それぞれクラスとして設計できないかを検討する
10.4 意図がわからない名前
何を目的としているのか意味が分かりにくいと、目的駆動名前設計の観点で考えると、関心の分離に貢献しない、責務が混乱し、密結合になる
-
技術駆動命名
技術ベースでの命名を技術駆動命名と呼ぶ -
驚き最小の原則
驚き最小の原則と呼ばれる設計原則があり、使う側が想像した通りに、予想外な驚きが最小になるように設計する考え方。予想通りにストレスなく利用できるよう、ロジックと名前を対応付ける設計が必要
10.5 構造を大きく歪ませてしまう名前
-
データクラスに陥る名前
データクラスは意図を理解した上で使い分けられることが肝要 -
クラスが巨大化する名前
分岐ロジックが実装されていれば、単一責任原則違反であり、責務の異なるロジックは他のクラスとして定義する -
状況によって意味や扱いが異なる名前
どんなコンテキストが取り巻くのか業務分析し、コンテキストごとに境界付けし、コンテキストに応じたクラスを設計すること -
連番命名
クラスやメソッドに対して番号付けで命名するのを連番命名と呼ぶ
10.6 名前的に居場所が不自然なメソッド
メソッドには、居場所が相応しくない、別のクラスに移動させるべきものがある、居場所が不相応か、名前でわかるものがある
-
「動詞+目的語」のメソッド名に注意
「動詞+目的語」の命名は、無関係な責務のメソッドを追加しやすくなる -
可能な限り動詞1語で済む名前にする
関心ごとの異なるメソッドの混在を防ぐには、可能な限り動詞1語で済むような名前設計にするのがコツ。同時に、動詞1語で済むようにクラス設計する
具体的には以下の法則に従う
「動詞+目的語」のメソッド
↓
目的語の概念を表現するクラスを作る
そのクラスに、動詞1語のメソッドを追加する
- 不適切な居場所のbooleanメソッド
「動詞+目的語」メソッドと同様に、boolean型を返すメソッドも、適切ではないクラスに定義されることがよくある
boolean型メソッド追加の際は、「クラス名is状態」の形に読み替えて、自然な英語として読めるかどうか確かめてみること
10.7 名前の省略
-
意図がわからなくなる省略
どこかにコメントやドキュメントがあればわかるが、ない場合は周辺ロジックから類推しなければならず、調査時間を浪費してしまう -
基本的には名前は省略しないこと
長い名前はタイプ文字数が多く、タイポしやすい側面があったので、一昔前は嫌われていたが近年では入力保管できるエディタが一般的でタイポの心配やタイピングの労力は気にならないため省略をせず書いたほうがいい -
その他省略をどう判断するか
省略をどの程度許容するかに関してはさまざまな考え方があるが、可能な限り省略せずに意図を伝える命名が望ましい
11章 コメント -保守と変更の正確性を高める書き方-
11.1 退化コメント
実装と比べてコメントの情報が古くなった時点で、コメントは嘘をつき始める。このように、情報が古くなり実装を正しく説明しなくなったコメントを退化コメントと呼ぶ
-
コメントは劣化コピーに過ぎないことを理解すること
プログラミングに限らず一般的に、コミュニケーション上、会話でも文章でも、どんな形であれ言葉は話者や書き手の意思の劣化コピーである。当然クラスやメソッドの名前、コメントも劣化コピーであり、伝えたい内容の情報が劣化していくため可能な限り制度良く意図を伝えられるクラスに命名したり、コメントしたりすること -
ロジックの挙動をなぞるだけのコメントは退化しやすい
ロジックの挙動をなぞるだけのコメントは理解にさほど貢献しない上、逆に偽情報が紛れ込んで害をなす可能性がある
11.2 コメントで命名を誤魔化す
ダメなメソッドに対してコメントで細く説明するのではなく、メソッド名事態をブラッシュアップするべき
メソッドの可読性を上げることで、再説明のコメントが不要になり、退化コメントが生じにくくなる
11.3 意図や仕様変更時の注意点を読み手に伝えること
コードが読まれるのが多い機会は、保守と仕様変更時
コード保守の際、読み手が気にするのは「このロジックはどういう意図で動いているのか」で、仕様変更の際、読み手が気にするのは「何に注意すれば安全に変更できるか」で、これらの課題を解決できるよう、意図や仕様変更時の注意点をコメントする
11.4 コメントのルールまとめ
ルール | 理由 |
---|---|
ロジック変更時、同時に必ずコメントも変更すること | コメントを変更しないと、ロジックと乖離した「退化コメント」が生じ、読み手が混乱するため |
ロジックの内容をなぞるだけのコメントをしないこと | あまり可読性に貢献しない上、コメントのコンテナンスが大変になるため、退化コメントも発生しやすい |
可読性の悪いロジックを細く説明するようなコメントをしないこと、代わりにロジックの可読性を高めること | コメントのメンテナンスが大変になるため、退化コメントも発生しやすい |
ロジックの意図や仕様変更時の注意点をコメントすること | 保守や仕様変更時の助けになる |
12章 メソッド(関数) -良きクラスには良きメソッドあり-
メソッド設計の良し悪しはクラス設計に密接に連動する、メソッド設計が良くないと余波でクラス設計も悪くなり、逆もまた然り
12.1 必ず自身のクラスのインスタンス変数を使うこと
メソッドは必ず自身のクラスのインスタンス変数を使うよう設計しよう、例外もあるが原則である
12.2 普遍をベースに予期せぬ動作を防ぐ関数にすること
可変なインスタンス変数などを変更する関数(メソッド)は、意図せず別の箇所に影響を及ぼし、予測しない動作が生じる場合がある、結果の予測が難しく、保守が大変になる
不変による堅牢性を生かして、予期せぬ動作を防ぐメソッドを設計すること
12.3 尋ねるな、命じろ
12.4 コマンド・クエリ分離
メソッドはコマンドまたはクエリのどちらか一方だけを行うように設計する、コマンドとクエリを分離する考え方
12.5 引数
-
引数は不変にすること
引数にはfinal修飾子を付与し、不変にしよう。引数を変更したい場合は、不変なローカル変数を用意し、そのローカル変数に変更値を代入する実装にしましょう -
フラグ引数は使わない
フラグ引数つきのメソッドは、何が起こるか読み手に想像を難しくさせる、何が起こるのかを理解するには、メソッド内部のロジックを観に行かなければならない -
nullを渡さない
引数にはnullを渡さない設計にしよう -
出力引数を使わない
出力引数を使うと低凝集構造に陥る、引数は入力値として用いるのが基本で、出力値として用いると読み手が混乱する -
引数は可能な限り少なく
多くの引数を使いたいメソッドでは、引数を使う分だけ処理内容が増大することを意味する。処理内容が増えるとロジックが複雑化し、さまざまな悪魔の温床になる
12.6 戻り値
-
「型」を使って戻り値の意図を表明すること
-
nullを返さない
引数にnullを渡さないのと同様に、nullを返さないようにしよう -
エラーは戻り値で返さない、例外をスローすること
13章 モデリング -クラス設計の土台-
動作原理や仕組みを簡単に理解・説明するために、物事の特徴や関係性を図式化したものをモデル、モデルを作る活動をモデリングと呼ぶ
13.1 邪悪な構造に陥りがちなUserクラス
大体はうまくモデリングされていないのが原因
13.2 モデリングの考え方とあるべき構造
モデルはシステム構造の説明のために用いる
-
システムとは何か
目的達成を効率化するためにシステムは作り出される、システムは目的達成のための手段である -
システム構造とモデリング
システムは目的達成手段で、モデルはシステムの構成要素です。つまり、モデルは目的達成手段の一部で、特定の目的達成のために最低限考慮が必要な要素を備えたのがモデルである -
ソフトウェア設計におけるモデリング
13.3 よくないモデルの問題点と解決方法
複数の目的のために無理やり利用されており、モデルングしているようでモデリングしていないクラス(モデル)は一貫性がないモデルである
-
Userとシステムの関係
-
仮想世界を表現する情報システム
情報システムとは、現実世界の概念のみをコンピューターの世界へ投影した仮想現実である -
目的別にモデリングする
情報システムでは、現実世界での物理的な存在と、情報システム上のモデルが1:1になるとは限らず、1:多の関係になるケースがあることが大きな特徴
目的駆動で名前設計することが、適切に目的達成するモデルを設計することにつながる -
単一責任とは単一目的
目的と責任はついになっていると言え、単一責任の原則とは、単一目的の原則であると考えられる。「クラスが果たす目的は、たった一つに限定すべきである
クラスは、特定の目的に特化して設計することで、変更に強う高品質な構造になる
システムは何らかの目的を達成するために作られるのであり、責務よりも目的が先に来る -
モデルの見直し方
・そのモデルが達成しようとしている目的を全て洗い出す
・目的それぞれ特化したモデリングをし直す
・目的駆動名前設計に基づき、モデルに命名する
・モデルに目的外の要素が入り込んでいる場合、さらに見直す -
モデルと実装は必ず相互にフィードバックする
クラス設計や実装で気づいたことは必ずモデルにフィードバックする、フィードバックすることで、モデルの正確性が向上する
13.4 機能性を左右するモデリング
機能性とはソフトウェア品質特性の一つで、顧客のニーズを満たす度合いである
-
裏に隠れた真の目的を見破る
-
機能性をイノベートする「深いモデル」
優れた変換能力を備えるようモデルを設計することが、機能性の革新につながると考えられる、このように本質的課題を解決し、機能性の革新に貢献するモデルを、ドメイン駆動設計では深いモデルと呼ぶ
14章 リファクタリング -既存コードを成長に導く技-
14.1 リファクタリングの流れ
リファクタリングとは、外から見た挙動を変えずに、構造を整理すること
-
ネストを解消し、見通しをよくする
条件を判定するif文が多重にネストしていると非常に見通しの悪い構造になる -
意味のある単位にロジックをまとめる
条件チェックと値の代入ロジックをそれぞれまとめ、条件チェックが全て完了した後に値を代入するようにロジックの順番を変える -
条件を読みやすくする
-
ベタ書きロジックを目的を表すメソッドに置き換える
ロジックはべた書きせず、目的を表すメソッドにまとめる
14.2 ユニットテストでリファクタリングのミスを防ぐ
もっとも確実性の高い手段の一つにユニットテストがある、ユニットテストとは小さな機能単位で動作検証するテストの総称。テストフレームワークやテストコードを用い、メソッド単位で動作検証する手段をもっぱら意味する
-
コードの課題を整理する
-
テストコードを用いたリファクタリングの流れ
テストコードを用いたリファクタリング方法は以下
1.あるべき構造の雛形クラスをある程度作る
2.雛形クラスに対してテストコードを書く
3.テストを失敗させる
4.テストを成功させるための最低限のコードを書く
5.雛形クラス内部でリファクタリング対象のコードを呼び出す
6.テストが成功するよう、あるべき構造へロジックを少しずつリファクタしていく
14.3 あやふやな仕様を理解するための分析方法
ユニットテストを用いたリファクタリングは、初めから仕様がわかっている前提があったのでテストを書けた、一方で実際の開発では、仕様がわからない場合がある。使用が分からなければ安全にリファクタリングするためのテストコードをかけない。どのようにすればいいのか
-
仕様化テスト
仕様化テストは、メソッドの仕様を分析するための手法
実際には、仕様化テストだけで全ての仕様を明らかにするのは難しい。メソッド内部のロジックや、どういうユースケースから呼び出されているかなど、複合的な分析を経て使用を明確化していくものですが、使用の手掛かりを掴む有効な分析手段の一つである -
試行リファクタリング
ロジックの意味や構造を分析するためにお試しでリファクタリングするもの
以下の利点がある
・可読性が上がり、ロジックの仕様理解が進む
・あるべき構造が見えてくる。どの範囲をメソッドやクラスに切り出せばいいかが見えてくる。つまり本番リファクタリングのゴールが見えてくる
・無駄なコード(デッドコード)が見えてくる
・どのようにテストコードを書けばよいかが見えてくる
14.4 IDEのリファクタリング機能
-
リネーム(名前の変更)
クラスやメソッド、変数の名前を一度に全て、正確に変更するリファクタリング機能 -
メソッド抽出
ロジックの一部をメソッドとして切り出す機能
14.5 リファクタリングで注意すべきこと
-
機能追加とリファクタリングを同時にやらない
-
スモールステップで実施する
-
無駄な仕様は削除することも視野に
15章 設計の意義と設計への向き合い方
15.1 本書は何の設計について書いたものなのか
保守性の中でも特に変更容易性の向上を目的とした設計手法
15.2 設計しないと開発生産性が低下する
変更が困難で壊れやすいコードをレガシーコードと呼び、レガシーコードが蓄積している状態を技術的負債と呼ぶ
-
バグを埋め込みやすくなる
・低凝集な構造によって仕様変更時に修正漏れが起きやすくなり、バグになる
・コードの理解が難しいため、実装ミスが起こりやすく、バグになる
・不整地が容易に混入する構造になりがちで、バグが起こりやすくなる -
可読性が低下する
・ロジックの見通しが悪く、読み解くのに時間がかかる
・関係し合うロジックがあちこちに散財しているため、仕様変更に関連するロジックを全て探し回る手間が増える
・不正値の混入でバグが発生した場合に、どこから不正値が混入したのか追跡が困難になる -
木こりのジレンマ
-
一生懸命仕事した感覚だけが残って生産性は悪いまま
-
国家規模の経済損失
15.3 ソフトウェアとエンジニアの成長性
-
エンジニアにとっての資産とは何か
技術力に他ならない -
レガシーコードに人は引きずられやすい
-
レガシーコードは高品質設計を妨げる
-
レガシーコードは開発工数を減少させる
15.4 課題を解決する
-
課題が見えないとそもそも設計する意識が生まれない
-
知覚容易な課題と知覚困難な課題がある
ソースコードの読解スキルと技術的負債の近くスキルは別 -
理想系を知って初めて課題を知覚できる
課題とは、理想と現状のギャップ。つまり、理想を知っていれば現状と比較でき、課題がわかる。理想的な設計と現状を比較することで、技術的負債が知覚可能になる -
変更容易性を比較できないジレンマ
15.5 コードの良し悪しを判断する指標
コードの複雑さや、可読性などの一連の品質指標をコードメトリクス、またはソフトウェアメトリクスと呼ぶ
-
実行可能コードの行数
メソッドが10行以内
クラスが100行以内など
そのプログラミング言語によって違いはあるが大体同じくらいになる -
循環的複雑度
コードの構造的な複雑さを示す指標で、条件分岐やループ処理が増える、ネストすると複雑さを増大していく -
凝集度
モジュール内における、データとロジックの関係性の強さを表す指標 -
結合度
モジュール間の、依存の度合いを表す指標 -
チャンク
人間の記憶は、短期記憶と長期記憶に分類され、近年の認知心理学の研究では、人間の短期記憶は一度に4+-1個の概念しか把握できない、という説がありこの個数をマジカルナンバー4と呼ぶ。また、記憶個数の単位をチャンクと呼ぶ
15.6 コード分析をサポートする各種ツール
- Code Climate Quality
- Understand
- Visual Studio
15.7 設計対象と費用対効果
-
パレートの法則(80:20の法則)
重要な機能は顧客が着目し、改善ニーズも当然多く上がってくる。そしてニーズを受けて頻繁に使用が変更される。こうした重要かつ仕様変更が頻繁に発生する箇所を狙って設計改善すれば、変更コストが抑えられ、費用対効果が高くなると考えられる -
サービスの中心的領域、コアドメイン
サービスの売りになるビジネス領域をコアドメインという
・システム内で最大の価値を付加すべき場所
・価値があり重要で、費用対効果が最大の箇所
・競争優位性があり、差別化が図られ、ビジネス上優位に立つポイント -
重要設計対象の選定にはビジネス知識が必要
16章 設計を妨げる開発プロセスとの戦い
コミュニケーション
-
コミュニケーションが希薄だと設計品質に問題が生じる
メンバー同士のコミュニケーションに問題があると、バグが増大する傾向にある -
コンウェイの法則
「システムの構造が、それを設計する組織構造に似てくる」という法則 -
心理的安全性
「自分が発言することを恥じたり、拒絶されるなど、不利益を被ることがないことをチームで共有されている心理状態」や「安心して自由に発言したり、行動できる状態」
16.2 設計
- 「早く終わらせたい」心理が品質低下の罠
- 粗悪なコードは綺麗なコードを書くより常に遅い
- クラス設計と実装のフィードバックサイクルを回す
- 厳密に設計しすぎず、サイクルを回し続けるのがコツ
- 「パフォーマンスが落ちるからクラスを追加しない」は正しいのか
- 設計ルールを多数決で決めるとコード品質は最低になる
- 設計ルールづくりのポイント
・設計ルールは、パフォーマンスやフレームワークの制約など、様々な要件とトレードオフになる可能性がある
・何が何でもルール絶対遵守ではなく、落とし所、妥協点の模索が必要な状況がある
16.3 実装
-
割れ窓理論とボーイスカウトの規則
1.建物に割れた窓が1枚ある
2.割れた窓が長く放置されていると、誰も気にかけていない象徴になる
3.他の窓が破られる、ゴミを捨てられるなど軽犯罪が起こり、次第に治安が悪化する
4.さらにエスカレートし、巨悪犯罪が起こるようになる
ボーイスカウトの法則は「キャンプ場を、自分が来た時よりも綺麗にすること」 -
既存コードを信用せず、冷静に正体を見破る
-
コーディング規約を利用しよう
-
命名規約
16.4 レビュー
-
コードレビューを仕組み化しよう
-
コードを設計視点でレビューしよう
-
敬意と礼儀
・能力と善意を想定する
・理由を説明する
・人を辱めない
・極端な言葉やネガティブな表現を使わない -
定期的に改善タスクを棚卸すること
16.5 チームの設計力を高める
-
影響力を持つレベルにまで仲間を集める
-
基本はスモールステップ
-
実感が大事、手を動かしてみよう
-
フォローアップ勉強会を開いてみよう
1.本に書かれているノウハウを1,2個程度読み合わせる
2.ノウハウを適用できそうな箇所を、プロダクションコードの中から探す、参加者それぞれが普段触っているコードであればとっつきやすくて良い
3.ノウハウを使ってコードを改善してみる
4.どう改善したかについて、before,afterがわかるように発表する
5.発表内容について質疑応答や議論をする -
勉強会のバッドノウハウ
-
リーダーやマネージャーに設計と費用対効果の話をする
-
設計責任者を立てる
・設計品質に関わるルールや開発プロセスの策定
・ルールの周知、教育
・リーダー・マネージャー層への共有
・品質の可視化
・設計品質の維持
17章 設計技術の理解の深め方
終わりに
よいコードの書き方、アンチパターンなどを詳しく書いておりそれを元にして設計をどのようにしていけばいいのかが具体的に書かれており本書で設計の理解が深まった
参考
良いコード/悪いコードで学ぶ設計入門