良いコード/悪いコードで学ぶ設計入門
備忘録としてメモする
悪魔を招きやすいコード例
- Mermoryやintなど技術用語が含められる技術駆動命名
- 001,002, 003のように番号付けで命名する連番命名
- if文によって何重にもネストされたロジック
様々な悪魔を招くデータクラス
まずデータクラスとは・・・データを保持するだけのクラスjavaで言う@Dataを使うようなクラス
データクラスに関係するものを別のクラスで表現しようとするとどうなるか?(低凝集)
- 同じロジックを複数箇所で実装してしまう恐れがある(重複コード)
- 重複コードの修正漏れ
- 関連コードを探すのに時間がかかる(可読性の低下)
- 初期化しないと使い物にならないクラス、初期化状態が発生しうるクラス(生焼けオブジェクト)が出来てしまう可能性がある→javaの@Builderとかは生焼けオブジェクトを作る良い例かも
- 何度も作るため不正値を含む可能性がある。バリデーションを作ればいいがそのバリデーションも低凝集を引き起こす可能性が、、、
データクラスの悪魔が次々に小さい悪魔を呼び寄せる
では悪魔の対処法は?
悪しき構造の弊害の把握・オブジェクト指向のクラスを適切に設計すること
まずは変数・関数単位の小さな設計から
ダメージの変数としてint damege = 1
とするべきなのに「amega」をサボってint d=1
のように書くのは可読性が下がりトータルで開発に要する時間が増大する
意図が伝わる名前を付けること
変数に再代入は、変数の用途が変わってしまう可能性があり、混乱を招く可能性がある
目的ごとに変数を用意する
一連の処理の流れをベタ書きするとどこからどこまでが何の処理か分かりにくい、使うべきでない変数を使ってしまいバグを生みやすくなる。これを防ぐためには意味のある出来るだけ小さいロジックでひとまとめにする。
意味のあるまとまりでメソッド化する
これらを守ることで文字数は多いが理解しやすいコードが出来る
しかし、メソッドだけだと今度は変数やメソッドがバラバラになってしまう
そこで、出てくるのがデータをインスタンス変数・変数を操作するメソッドをひとまとめにするクラス
クラス内でバリデーションも行えば変数のバグ化も防げる
保守・変更がしやすいコードを書くには関心の分離が重要
クラス単体で正常に動作するよう設計する
ドライヤーや電子レンジなどのように、その製品単品で正常に動作するように設計する。
初期設定や不正状態にならずに操作できる状態を目指すべき
あるべきクラス設計
良いクラスの構成要素
- インスタンス変数
- インスタンス変数を不正状態から防御し正常に動作するメソッド
例外を除いて基本的にはどちらも満たさなくてはいけない
##なぜ?
-インスタンス変数を操作するロジックが別のクラスで実装→関連しあうもの同士の認知の困難を引き起こす→重複コードの発生・修正漏れetcの弊害を招いていた
-どんな値でもインスタンス変数に出し入れ可能だったので不正値からの防御力が0に近い
-他のクラスが準備してやらないと正常動作しない未熟なクラス
になってしまう
未熟なクラスにしないためには自己防衛責務を持たせる必要がある
成熟したクラスへ成長させる設計術
- コンストラクタで確実に"正常値"を設定する
(処理の対象外となる条件をメソッドの先頭に定義する方法をガード節という) - 計算を行うロジックはデータ保持側に寄せる
- インスタンス変数を不変(const・final)にすることによって思わぬ動作を防ぐ
- 変更したい場合は新しいインスタンス変数を作成する
- メソッド引数やローカル変数も不変にする
- 値の渡し間違いを型で防止(プリミティブ型ではなく独自の型で値の受け渡しを)
例えば、intを引数にした場合、金額を受け渡したくても、チケットの数を受け渡してしまう可能性がある。こういった事故を防ぐために金額を受け渡したい場合は、Maneyクラスを作って引数の型に使うようにする。 - システムに必要なメソッドのみを定義するように
これによって悪魔達はどうなったか
悪魔 | どうなったか |
---|---|
重複コード | 必要なロジックが必要とするクラスに集まるため、別のクラスに重複コードが書き散らされにくくなった |
修正漏れ | 重複コード解消に伴い修正漏れもはっせいしにくく |
可読性の低下 | 必要なロジックは必要とするロジックにあつまるためデバック時などに関連ロジックを探し回らずに済むため可読性の向上 |
生焼けオブジェクト | コンストラクタでインスタンス変数の値を確定し、未初期化状態が無くなった |
不正値の混入 | ガード節や不変にすることで不正値混入をブロック |
思わぬ副作用 | 不変にすることで副作用から解放 |
値の渡し間違い | 引数を独自の型にすることで、異なる型の値をコンパイラで防止 |
高凝集:密接に関係しあうロジックが一か所に集まっていること
カプセル化:データとそのデータを操作するロジックを一つのクラスにまとめメソッドのみを外部公開すること
設計パターン
設計パターン | 効果 |
---|---|
完全コンストラクタ | 不正状態から防護 |
値オブジェクト | 特定の値に関するロジックを高凝集に |
ストラテジ | 条件分岐を削減し、ロジックを単純化 |
ポリシ | 条件分岐を単純化したり、カスタマイズできるようになる |
ファーストクラスコレクション | 値オブジェクトの亜種。コレクションに関するロジックを高凝集に |
スプラウトクラス | 既存のロジックを変更せずに安全に新機能を追加する |
可変(ミュータブル):変数の値を変更するなど状態を変更できること
不変(イミュータブル):変数の状態を変更できないこと
可変と不変を適切に扱わないと悪魔が暴れ出す
可変がもたらす意図せぬ影響
- 可変インスタンスの使いまわし
例:attackインスタンスを元に作成しているweaponA, weaponBがある。weaponAの攻撃力を変更しようとしてattackインスタンスを変更するとweaponBを変更する気はないのに同じattackインスタンスをつかってるためweaponBの攻撃力も変わってしまう - 関数による可変インスタンスの操作
関数が引数を受け取り、戻り値を返す以外に外部の状態を変更する副作用を持っていると起こる
*ただし、関数内で宣言したローカル変数の変更は副作用ではない
不変と可変の取り扱い方針
- 標準は不変で、間違った使い方が出来ないフールプルーフの立場を採る
- 大量のデータの高速処理などインスタンスを再生することによりパフォーマンスに問題が生じるケースは可変でも許される
- スコープがループカウンタなどの局所的なケース
コード外のやり取りは局所化する
局所化の方法1 : リポジトリパターン
リポジトリパターン:データベースの永続化処理をカプセル化する設計パターン
凝集度
凝集度:モジュール内における、データとロジックの関係性の強さを表す指標
この本では「クラス内におけるデータとロジックの関係性の強さを表す指標」とする
高凝集→変更に強い、望ましい設計
低凝集→壊れやすく変更が困難
staticメソッドの誤用
staticメソッドの誤用により低凝集に陥るケースがある
staticメソッドはインスタンス変数を扱えない=staticメソッドを持ち出した時点でデータとデータを操作するロジックが乖離する=低凝集になる
インスタンスメソッドのふりをしたstaticメソッドにも注意が必要
見破る方法:
staticキーワードを追加する。するとインスタンス変数が使われてれば「インスタンス変数が使われている」とエラーが出たり、コンパイルが通らなくなる。エラー表示もなし、コンパイルも通るならばそいつはstaticメソッド
staticメソッドの使い時
ログ出力用のメソッドやフォーマット変換用メソッドなど凝集度に影響がないものはstaticメソッドにしてもいい
コンストラクタの公開は関連ロジックが分散し、低凝集になる可能性がある
これを防ぐには?
コンストラクタをprivateにして、目的別のファクトリメソッドを用意する
ファクトリメソッドなどの生成ロジックが増えすぎたらファクトリクラスを検討する
クラス内で生成以外のロジックの存在が薄くなるぐらいなら生成専門のファクトリクラスを作るのもあり
性質はstaticメソッドと同じ低凝集構造
低凝集になりグローバル変数が出現しやすくなるなど悪影響は多岐にわたる
##より質の悪い悪魔を呼び込む共通処理クラス(CommonクラスやUtilクラス)に注意共通処理クラスは関連のないロジックが同じクラスに雑多に実装されてしまう=低凝集な構造を多く作りこんでる状態
根本的な原因は
再利用性は高凝集な設計にすることで高まる
という共通化や再利用性に関して理解していないこと
横断的関心事に関する処理であれば共通処理としてまとめ上げてもいい
結果を返すために引数を使わないこと
引数の使い方を誤ると低凝集に陥りやすい
引数の値を変更し出力するプログラムはメソッドの中身を気にしながら開発しなくてはいけない
=ロジックを読み解く時間を増大させる=可読性の低下を招く
多すぎる引数に注意
引数が多すぎるメソッドは低凝集に陥る良くない構造
引数が多いということは処理が簡潔ではないこと=ベタ書きのメソッドバージョン=重複コードが作成される危険性があること
引数が多い場合は概念的に意味のあるクラスにまとめる=データを引数として扱うのではなくデータをインスタンス変数として持つクラスへ設計変更する
プリミティブ型執着
プリミティブ型執着:プリミティブ型を乱用したコードのこと
プリミティブ型だけで実装すると重複コードや演算ロジックがあちこちに無秩序に実装されやすくなる
プリミティブ型だけでも動くコードは作れる。でも、強く関係しあうデータとロジックを凝集できない
結果として、バグを埋め込みやすく、可読性が低下する
メソッドチェイン
メソッドチェイン:「.」で数珠つなぎにして戻り値にアクセスする書き方。
低凝集に陥る良くない書き方
要素に仕様変更が起きた場合に呼び出し箇所をすべて見て影響を調べなければならない
デメテルの法則(知らない人に話しかけるなとするもの)に違反している
ソフトウェアには「尋ねるな、命じろ(Tell, Don't Ask.)」という格言がある
呼び出し側はメソッドで命じるだけで命令された側で適切な判断や制御するように設計する
条件分岐の周りに潜む悪魔
- 条件分岐による可読性の低下
条件分岐のネストは可読性の低下により、開発生産性の低下、ロジック変更の際のバグの誘因を引き起こす
この悪魔を退治する方法の一つが早期return(条件を満たさない場合さっさとreturnする)である。
早期returnは条件ロジックと実行ロジックを分離できる利点もある
見通しを悪くするelse句も早期returnで解決
else if文なども見通しが悪くなる原因。早期returnにすることでif文のみにすることも可能になる。
悪魔を呼ぶSwitch文
同じような条件式のswitch文が複数実装されるのは良くない兆候。仕様変更時の修正漏れが起こる。
条件分岐を一箇所にまとめる
switch文の重複コード解消には"単一責任選択の原則"が重要になる
単一責任選択の原則:ソフトウェアシステムが選択肢を提供しなければならない時、そのシステムの中の1つのモジュールだけがその選択肢の全てを把握すべきであるという原則
簡単に言うと同じ条件式の条件分岐は複数書かず、一箇所にまとめよう!という原則
switch文重複を解消するinterface
interfaceは関心事の分割に役立つ
interfaceを使うと分岐ロジックを書かずに分岐と同じことが実現可能になる
機能の切り替えは条件分岐を書かずに多態性が勝手にしてくれるから
interfaceの命名について
- 何の仲間であるかがinterface命名の決めてとなる
また、switchの代わりにMapを使うことでさらに切り替えが把握しやすくなる
このようにinterfaceを用いて処理を一斉に切り替える設計を"ストラテジパターンという"
ストラテジパターンのメリット
- switch文を低減してくれる
- 未実装のメソッドがあればコンパイラが叱ってくれる
条件分岐の重複とネスト
interfaceはswitch文の重複解消以外にも、ネストした条件分岐の解消にも役立つ
同じ判定ロジックを再利用できないものか
そこで役立つのが'"ポリシーパターン"
条件の部品化、部品化した条件を組み替えてのカスタマイズを可能にする
<実践するのは結構むずそう>
リスコフの置換原則
リスコフの置換原則:基本型を継承型に置き換えても問題なく動作しなければならないとする原則
分岐を書きそうになったら、まずはinterface設計
フラグ引数
フラグ引数付きメソッドは、何が起こるか読み手に想像させにくい
=可読性が低下し、開発生産性が低下する
フラグ付きメソッドは内部に複数の処理を持つのが原因
→メソッドが単機能になるように設計すればよい
処理の切り替え機構は丁寧にストラテジパターンで設計
ループ処理中の条件分岐ネストは早期continueで解消
早期returnの時と同じように早期continueを行うと条件分岐のネストを解消できる
同様に、早期breakも有効
低凝集なコレクション処理
コレクション処理も低凝集に陥りやすい。
解決方法としては、コレクションに関連するロジックをカプセル化するファーストクラスコレクションにする
ファーストコレクションクラスには以下の要素が備わってる必要がある
- インスタンス変数(前にも言った)
- インスタンス変数を不正状態から防御し、正常に操作するメソッド(前にも言った)
- コレクション型のインスタンス変数
- コレクション型のインスタンス変数を不正状態から防御し、正常に操作するメソッド
外部へ渡す場合はコレクションを変更できなくする
インスタンス変数をそのまま外部へ渡すと、コレクションに追加したり、削除したり勝手な操作を許してしまう。そこで外部へ渡す際は、コレクション要素を変更できなくする。(unmodifiableList()を後ろにつける)
密結合
結合度:モジュール間の、依存度合いを表す指標
あるクラスが他の多くのクラスに依存している構造を”密結合”という
密結合はコードの理解が難しく、変更が大変
密結合の問題を解決する上で「責務」の考えは欠かせない
以下のようなクラスは責務を考慮されていないクラス
- 一部のクラスに処理が集中している
- 別のクラスでは何も処理を行っていない
- 他のクラスの一部のメソッドを都合よく使ってる
ソフトウェアにおける責任とは「ある関心事について、不正な動作をしないように、正常に動作するよう制御する責任」
ここで重要な役割を果たすのが「クラスが担う責任は、たった一つに限定するべき」
とする設計原則である"単一責任の原則"
過保護な毒親のような責任を多重に負うクラスは、他のクラスは未熟になり、重複コードが量産される
同じようなロジックが複数あるからと言って責務を考えずにひとまとめにすると責務が多重になる
DRY原則(Don't Repeat Yourself、繰り返しを避けよ)という原則がある。これは似ていれば全ての重複を許すなという意味ではなく 「同じようなロジック、似ているロジックであっても、概念が違えばDRYにすべきではない」という意味である。
密結合の各種事例と対処方法
- 継承に絡む密結合
継承は注意して扱わなければすぐに密結合になってしまう。
継承関係にあるクラス同士ではサブクラスはスーパークラスに依存し、
サブクラスはスーパークラスの構造を気にしなくてはいけない
スーパークラス依存による密結合を避けるため「継承よりも委譲」が推奨される
委譲とはコンポジション構造にすること
ある継承クラスにとっては関係があっても、
別の継承クラスにとっては無関係なメソッドが登場しはじめると危険信号
製品に用いられるコードはもっと泥臭く、混乱しやすく、依存関係が複雑
上手く別クラスに分離するにはどのインスタンス変数やメソッドがそれぞれ何に
関係付けられてるか把握することが大事
影響スケッチを書くと良いかも
なんでもパブリックで密結合
なんでもかんでもpublicにすると、関係し合って欲しくないクラス同士が結合し、影響範囲が拡大してしまう
アクセス修飾子で可視性を適切に制御することが密結合を避けるコツ
アクセス修飾子はどうしてデフォルトだとpackage private?
パッケージ同士の不要な依存を避けるにはpackage privateが適切だから。
パッケージは強く関連付くクラス同士を凝集するように設計する。
そのため、パッケージ外部からはアクセスさせたくない。するとpackage privateがふさわしくなる
private メソッドが多いクラス
private メソッドが多いクラスは単一責任ではなく多くの責務を持ってしまってる。
高凝集の誤解により密結合になってしまうケース
高凝集を意図して強く関係していそうなロジックを一箇所にまとめようとしたものの、
結果として密結合に陥ってるケースは多くみられる、陥りやすい罠。
そこで設計においては「疎結合高凝集」とセットで扱う
スマートUI
表示関連のクラスの中に、表示以外の責務のロジックが実装されている構造をスマートUIと呼ぶ。
表示責務と表示以外の責務が密結合になっている。
開発初期などによく起こる。アップデートの際などにバグが起きる可能性が高くなる
巨大なデータクラス
大量のインスタンス変数を持っている。単なるデータクラスとは異なり、多くの悪魔達を呼び寄せる。
巨大なデータクラスは色んなデータをもってるので色んな所で使われる
=グローバル変数の性質を帯びてくる=排他制御のためにパフォーマンスが低下するなどの弊害を招く
トランザクションスクリプトパターン
トランザクションスクリプトパターン:メソッド内に一連の処理手順がダラダラ書かれる構造
データクラスとデータをしょりしているクラスで分けている場合に頻繁に実装される
低凝集密結合で変更が困難
神クラス
トランザクションスクリプトパターンの進化系
あらゆる責務のロジックが乱雑に絡み合うように書きなぐられてるクラス
悪魔の巣窟・密結合の権化
密結合クラスの対処法
巨大データクラスもトランザクションスクリプトパターンも神クラスも密結合なクラスの対処法は同じ
- オブジェクト指向設計
- 単一責任の原則
- 丁寧な設計
単一責任の原則を遵守するよう設計されたクラスはどんなに多くても200行、ほとんどは100行になる
設計の健全性をそこなうさまざまな悪魔達
デッドコード
デッドコード:どんな条件であっても決して実行されないコードのこと
デッドコードの引き起こす様々な弊害
- コードの可読性が低下する
- これまで到達不可能が到達可能になった際にバグになる可能性がある
IDEの静的解析機能にはデッドコードを検出する機能もある
YAGNI原則
YAGNI:「You aren't going to need it.(必要ないでしょう)」
実装に必要になったときにのみ実装せよという方針
YAGNI原則を守らないとどうなるのか?
- 予測が外れ使われなくなったコードはデッドコードになる
- 先回りで作られたロジックは、往々にして複雑。
先回りで作りこんだ分時間が無駄になる。今必要な機能だけを作り、構造をシンプルにするべき。
可読性が良くなり、保守変更が容易になり、無駄な工数もかからなくなる
マジックナンバー
マジックナンバー:ロジック内に直接書き込まれてる意図不明な数値。重複コードを生み出す原因になる。
マジックナンバーを書かないようにするには、定数として定義すればよい。
文字列型執着
読み込んだCSVからデータを取り出すためにsplitメソッドを使うケースはある。しかし、そういう意図もないのに、意味の異なる複数の値をString変数に無理に詰め込むと意味が分かりにくくなる。また、splitメソッドなどでロジックが無駄に複雑化し可読性が著しく低下する。
グローバル変数
一見便利に見えるが、どのタイミングで値が変わったのか把握が困難。グローバル変数を参照しているロジックにバグが生じないか見当もしなくてはいけない。検討の結果、排他制御が必要な場合も生じる。排他制御の設計にミスがあると、ロック時間が長くなってパフォーマンスが低下したり、デッドロックに陥る可能性がある。
グローバル宣言された変数だけでなく巨大データクラスのようにグローバル変数の性質を持っている変数もあり、知らず知らずのうちに使っている可能性があるから注意が必要
対処法
- 影響範囲が最小化するように設計する
- 無関係ロジックからはアクセスできないように設計する
呼び出し箇所が少なく、局所化されているほどロジックの理解が容易になる
どうしても使いたい場合は必要性をよく検討する。
null問題
nullが入り込むことを前提でロジックを組むと、いたるところでnullチェックしなければならないが、
nullチェックだらけでコードの見通しが悪くなるし、nullチェックが漏れるとバグになる
null例外によるnullチェックを避けるためにも、そもそもnullを返さない・渡さないを満たす設計にする
null安全
nullが原因のエラーを発生させない仕組み
例外の握り潰し
try-catchで例外をキャッチしても何の処理もせず、握りつぶすのは極めて邪悪なロジック
エラーが起こっても、外から検知するすべがなくなってしまう。
どのタイミングで、なんのコードが原因で不正状態になったのか分からず、データベースのレコードや各種ログ、関連しそうなコードを、追わなくてはいけない=開発者の時間と体力を著しく浪費させる
こうした結果を防ぐためにも不正状態に対して寛容になるべきでない。
状態が不正なまま通常処理を実行させるのは、導火線に火がついてるのも知らずに爆弾を持ってうろうろ歩き回るのと一緒。
設計秩序を破壊するメタプログラミング
プログラム実行時に、そのプログラム構造自体を制御するプログラミングをメタプログラミングという
メタプログラミングの技術の一つとして Javaではクラス構造を読み書きできるリフレクションがある
メタプログラミングは通常アクセスできない場所にアクセス出来るなど裏技的な面もあるので黒魔術とも呼ばれている。
リフレクションを乱用すると、不正状態から防御する設計、カプセル化を意識した設計が全く意味がなくなってしまう
静的型付け言語の強みとして、静的解析によるコード分析可能であることが強みであるがメタプログラミングはその強みさえも打ち消してしまう
IDEについて
IDEの静的解析により、どのクラスがどこから参照されているのか正確に分析できる。これにより名前変更機能や定義元へのジャンプ、参照箇所の全検索など開発効率化や正確性向上に役立っている
技術駆動パッケージング
パッケージの区切り方、フォルダの分け方に注意しないと悪魔を呼び寄せる原因になりかねない
設計パターンなど、構造的ににているもの同士でフォルダ分け、パッケージ分けするのを技術駆動パッケージングという(models、views、contorllerとで分けているのは技術駆動パッケージング)
技術駆動パッケージングは本来なら強く関連するはずのクラスが別フォルダにいってしまい、探し回る手間が増えるなどの問題が起きる
サンプルコードのコピペ
サンプルコードをコピペして実装すると設計上よくない構造になりがち
サンプルコードはあくまで言語仕様やライブラリの機能性を説明するために書かれたもので、保守性や変更容易性まで考えて書かれたコードではないことを覚えておく。
銀の弾丸
なんでもかんでも「自分が知っている便利な手法」を使うことがどうなるか。
>課題解決に対して貢献しないどころか、逆に問題を深刻化してしまうケースすらある。
ソフトウェアには銀の弾丸のような、やっかいな問題を撃退する特効薬のようなものはない
この本で載せられる手法は、仕様変更に伴う労苦の提言を目的としたものであり、
実験的に開発したプロトタイプや寿命間近で仕様変更が伴わないソフトウェアに対しては効果を発揮せず、
逆に設計コストが高くついてしまう
大事なのは、どんな課題があるか、ある手法がその課題解決に効果的かどうか、コスト的に問題にならないかを評価して判断する姿勢。設計にbestはないのでbetterを目指す
名前設計
適切な責務を与え、密結合を防止するには、クラスやメソッドへの命名も重要なポイント
この本で共通する設計はソフトウェアで達成したい目的をベースに
名前を命名する"目的駆動名前設計"である
目的駆動名前設計は、名前から目的や意図が読み取れることを特徴とする
悪魔を呼び寄せる名前
- 影響範囲が広すぎるために、開発生産性が低下してしまう名前
- 大雑把で意味がガバガバな名前は、あらゆるロジックを引き付ける。
影響範囲が広すぎると目的不明なクラスとなり目的不明オブジェクトになる
顧客向けのプロダクト開発における目的は「会社の事業的にどういう目的を達成したいのか」というビジネス目的になる。ビジネス特化にすることで以下の効果が生まれる
- 名前とは無関係なロジックを排除しやすくなる
- クラスが小さくなる
- 関係するクラスの個数が少なくなる=結合度が低減する
関係クラス個数が少ないので、仕様変更時に考慮を要する影響範囲が小さくて済む - 目的に特化した名前なので、どこを変更すれば良いかすぐ探し出せる。
- 開発生産性が向上する
存在ベースではなく、目的ベースで名前を考える
単純に存在を示すだけの名前は、意味が多重になりがちで、目的不明オブジェクトになる
ロジックレベルで混乱が起きる
ビジネス目的で特化した命名をするには、どんなビジネス目的があるか網羅する必要がある。
登場人物や事柄の列挙、関係性の整理、分析をするとよい
チームで集まって、ホワイトボードや模造紙に書いてみるのもいい
しかし、先述した分析活動には陥りがちな罠がある。名前が沢山書きだされても、慣れていかないと名前の背後にあるビジネス目的まで書き出されることはなかなかない。
名前も大事だが、どんな目的を達成したいのか、どう使われるか、何と関係するか、その理由など背景と意図の認識が整理され、チームと一致していることが重要
ラバーダッキング:問題が発生した際に説明すると自ら原因に気づき、自己解決する手法
積極的に話し合い、会話の中に特化した名前がないか注意深く耳をそばだて、名前や関心事を関連度で集めていくことが重要
利用規約を読んでみる
利用規約には、サービスの取り扱いやルールが極めて厳密な言い回しで書かれているので、特化した名前の参考になる。
疎結合高凝集になっているか点検する
目的に特化した名前を選ぶと、目的以外のロジックを寄せ付けにくくする効果がある
他のクラスと何個も関連付けられているのは良くない兆候のなので、他のクラスといくつ関連付けられているか個数を確認する
疎結合の注意すべきリスク
- 名前無頓着になるな
- 仕様変更時の「意味範囲の変化」に警戒
- 会話には登場するのにコード上には登場しない名前に注意
- 形容詞で区別が必要な時はクラス化のチャンス
命名で頻繁に陥りがちな様々な悪しきケースと、その対策方法
意図が分からない名前
計算結果を一時的に買う脳するためのローカル変数付けられるtmpなどは、何を目的としているのか意味が分かりにくい。目的駆動名前設計の観点で考えると、関心の分離に貢献せず、責務の混乱、密結合が生まれる
意図不明な名前は解釈の誤りを増大させ、誤った解釈に基づいて新たなバグを持ったロジックが実装される
意図不明な名前に陥るケース
- 技術駆動命名(ミドルウェアでのロジックなど仕方がない部分はしょうがない)
- 驚き最小の原則(使う側が想像したとおりに、予想外な驚きが最小にするようにする設計)
クラス構造を大きく歪ませてしまう名前
~infoや~Dataと命名されたクラスは「データだけ持たせるクラスなんだ、ロジックは実装しちゃダメなんだと読み手に印象付けてしまう」。データのみを想起させる名前は避けてProductionInfoやProductへ改善するべき
###DTO (Data Transfer Object)
一部例外的にデータクラスを用いる場合がある。
更新責務と参照責務でモジュールを分離したコマンド・クエリ責務分離(CQRS)と呼ばれるアーキテクチャパターンがある。CQRSにおいて、参照系とはデータベースから値を取得される時だけに用いられる
主に画面表示に用いられる
データベースの値を格納して表示側に転送するだけのクラスとして設計する
インスタンス変数は、値変更の必要がないためfinalで宣言し、コンストラクタで値が確定するようにする
参照系でのみの用途なので、更新系では使ってはいけない。※更新系で使うと低凝集になる
クラスの巨大化
単に関係してそう、というだけクラスを作ると異なるロジックがどんどん集まり出してくる。
結果として、あらゆるロジックのごった煮の無秩序状態が生まれる。
状況によって意味や扱いが異なる名前
アカウントという言葉が金融業界では口座を意味するのに対し、コンピュータセキュリティではログイン権限を意味する。このように状況によって言葉は意味合いが異なる
この現象には注意が必要で全く異なるもの同士が密結合になる可能性を含んでいる
コンテキストが違うものは違うもの同士は疎結合になるように設計する
連番命名
目的や意図が読み取れない点において技術駆動命名と似ているが、構造改善が困難な点で連番命名は悪質
例えば、技術駆動で命名されてたとしても目的駆動名前設計で名前を見直せる。名前を細分化して、目的に応じたクラスへの分割も可能できる。
連番命名はそうはいかない。クラスやメソッド番号で管理されているために、連番命名以外で命名すると番号付の秩序が失われる⇨番号で管理したい側から反発を招きかねない→名前見直しが困難になる
このような管理上の強制力から、使用追加のたびに既存メソッドへのロジック追加されるだけになる
=連番命名はトランザクションスクリプトパターンに容易に陥らせてしまう。
名前的に居場所が不自然なメソッド
メソッドには居場所が相応しくない、別のクラスに移動させるべきものがある。
動詞+目的語のメソッド名に注意
実装を急いでいる時、既存のクラスだけで動くように無理に実装しようとした時関心事に無関係なメソッドが追加されることがよくある。この関心事が異なるメソッドはaddItemToPartyのように「動詞+目的語」形式の名前になる傾向にある。
クラスを作成する習慣がないと、この傾向は輪にかけて顕著になる。メソッドの命名規律がないと、責務が異なるメソッドが際限なく追加されやすくなる。
可能な限り動詞1語で済む名前にする
関心事の異なるメソッドの混在を防ぐには可能な限り動詞1語で済むように名前設計するのがコツ
同時に、動詞1語で済むようにクラス設計する
不適切な居場所のbooleanメソッド
動詞+目的語メソッドと同様にboolean型を返すメソッドも適切ではないクラスに定義されることがよくある
関心事に注意を払わずにいるとboolean型を返すタイプの判定メソッドは責務外クラスに実装されがち
メソッドを定義するクラスが適切がどうかを見分ける方法
クラス名 is 状態が自然な英語として読めればよい
名前の省略
名前の省略には注意が必要
どこかにコメントやドキュメントがあれば分かるかもしれないが、ない場合は周辺ロジックから類推しなければならない、また、コメントがあってもメンテナンス不足や説明不足により分からなくなる可能性もある
メソッド名やクラス名、パッケージ名なども省略せずに書くべき。可読性が上がり将来の誰かを助ける
SNSやVIPといった慣習的に省略形が使われていて意味が通じるならばそれで良い
省略をどの程度許容するかに関してはさまざまな考えがある。
可能な限り省略せずに意図を伝える命名が望ましいと筆者は考えている
省略するなら意味混乱のリスクと相談して極めて小さければ採用してもよい
退化コメント
実装と比べてコメントの情報が古くなった時点で、コメントは嘘をつき始める。
このように、情報が古くなり実装を正しく説明しなくなったコメントを退化コメントという
コメントが偽情報として振る舞うようになるので、読み手が混乱する、結果としてバグを埋め込む
可能性が生まれる。退化コメントが発生しないように、実装に合わせてコメントも同時に更新すべきだが
以下のことに注意が必要
- コメントは劣化コピーに過ぎないことを理解すること
コミュニケーション上、会話でも文章でも、どんな形であれ言葉は話者や書き手の意思の劣化コピーに過ぎない。可能な限り精度よく意図が伝わるようにクラスに命名、コメントしなくてはいけない - ロジックの挙動をなぞるだけのコメントは劣化しやすい
コード変更するたびにコメントを更新しなくてはいけなくなる。また、ただの伝言ゲームになりがちで、
偽情報が紛れ込んで害をなす可能性がある - コメントで命名を誤魔化す
意味の伝わりにくい命名を再説明のコメントにより補足しても、それは補足にはならず退化コメントを発生させる。メソッドの可読性を上げることで、再説明のコメントが不要になるので。コメントより命名である程度説明できるように心がける。
意図や仕様変更時に注意点を読み手に伝えること
コードは保守・仕様変更時にコードが最も読まれやすい
この時、読み手は「何に注意すれば安全に変更できるのか」である。この課題を解決できるように、意図や仕様変更時の注意点をコメントする。コメントのルールをまとめると以下になる
ルール | 理由 |
---|---|
ロジック変更時、同時に必ずコメントも変更すること | コメントを変更しないと、退化コメントが生じ、読み手が混乱するから |
ロジックの内容をなぞるだけのコメントをしないこと | あまり可読性に貢献しない上、コメントのメンテナンスが大変になるため。退化コメントも発生しやすい |
可読性の悪いロジックを補足説明するようなコメントをしないこと。代わりにロジックの可読性を高めること | コメントのメンテナンスが大変になるため退化コメントも発生しやすい |
ロジックの意図や仕様変更時の注意点をコメントすること | 保守や仕様変更時の助けになる |
ドキュメントコメント
javadocなどのフォーマットにのっとってコメント記述するとドキュメントの自動生成できる。また、コメント内容をポップアップ表示出来たり、メソッド定義にジャンプしなくても、メソッド呼び出し側で説明コメントを参照できるようになり、可読性が飛躍的に高まる
メソッド - 良きクラスには良きメソッドあり-
必ず自身のクラスのインスタンス変数を使うこと
インスタンス変数を安全に操作するようにメソッドを設計することでクラス内の正常性を担保できる仕組み
基本原則として、メソッドは必ず自身のクラスのインスタンス変数を使うように設計する。
その点、getter/setterはよそのクラスを気にしたりいじったりするメソッド構造に陥りやすく、開発生産性が良くないソフトウェアのソースコードでは頻繁にみられる。「尋ねるな、命じろ」の考えのもと、呼び出されるメソッド側で複雑な制御をするように設計する。
コマンド・クエリの分離
状態の変更と取得を同時に行うメソッドは、混乱しやすい上に利用者にとっても使いにくい。取得だけしたい、変更だけしたいケースに対応できず、良いことがない。
コマンド・クエリ分離(CQS)と呼ばれる考え方がある。メソッドはコマンド(変更)またはクエリ(問い合わせ)のどちらか一方だけを行うように設計する考え方。
引数
引数の設計上の注意点を挙げる
- 引数は不変にすること
- フラグ引数は使わないこと
- nullを渡さないこと
- 出力変数は使わないこと
- 引数は可能な限り少なくすること
戻り値
- 「型」を使って戻り値の意図を表明すること
- nullを返さないこと
- エラーは戻り値で返すのではなく、例外をスローすること
モデリング -クラス設計の土台 -
動作原理や仕組みを簡単に理解・説明するために、物事の特徴や関係性を図式したものをモデル、
モデルを作る行動をモデリングという
モデリングをしないと変更に弱く、悪魔を呼び寄せるコードを容易に書いてしまいがち。
モデリングの考え方とあるべき構造
モデルはシステム構造の説明に用いる。したがって、モデリングの理解には、まずシステムがなんであるかを理解しなければならない
システムとは何か
世界は、様々な社会的活動によって成り立っている。移動する、仕事する、全ての行動はシステムによって実行する。目的達成を効率化するために作りだされ、目的達成するための手段である。システムの内、コンピュータを利用したものを情報システムと呼ぶ。
そして、モデルはシステムの構成要素である。特定の目的達成のために最低限の考慮が必要な要素を備えたものがモデルである。
良くないモデルの問題点と解決方法
- 一貫性がないモデル
複数の目的のために無理矢理利用されており、モデリングしているようでモデリングしていないモデル
解決方法としては、モデリングの際に対象とするものの観察や要素抽出が必要
解消の鍵は、情報システムが持つ、自動車や飛行機といった物理的システムとは大きく異なる特徴にある
情報システムのベースはコンピュータである。そのため物理的なやりとりはなく、概念的な事柄のみが01ビットとして表現されている。つまり情報システムというのは、現実世界の概念のみをコンピュータへ投影した仮想現実であるという特徴を持っている。現実世界の概念を仮想世界へ変換し、意味のマッピング、概念的なやりとりをコンピュータによって高速化することで効率化しているとも考えられる。
この考えに基づいて、商品や利用者は物理的な詳細などは無視され、概念的な側面だけが仮想現実に投影されたモデルとして解釈できる。
情報システムでは、現実世界での物理的な存在と、情報システム上のモデルが1:1になるとは限らず、
1:多の関係になるケースがあることが大きな特徴であり、注意すべき点
モデリングが上手くいかない原因として、モデルを単なるものとして解釈していることも挙げられる。
モデルは、目的他姓手段であるシステムの一部であるため、そう思うだけで上手くモデリングできるようになる
目的駆動で名前設計をすることが、適切に目的達成するモデルを設計することにつながる
単一責任は単一目的でもある。
特定の目的に特化して設計することで、変更に強い高品質な構造になる
責務、責務と言われてピンとこないときは目的を見直す。なぜなら、システムは何らかの目的を達成するために作られるのであり、責務よりも目的が先に来るから
モデルの見直し方
モデルにいびつさ、不自然さ、一貫性がない場合は以下を検討するとよい
- そのモデルが達成しようとしている目的をすべて洗い出す
- 目的それぞれ特化したモデリングをし直す
- 目的駆動名前設計に基づき、モデルに命名する
- モデルに目的以外の要素が入り込んでいる場合、さらに見直す
モデルと実装は必ず相互にフィードバックする
モデルは仕組みを単純化したものに過ぎない。細部は描写されない。したがってモデルに基づきクラスを設計しコードを実装する。モデル≠クラス。
クラスやコードに精微化していく段階で、動作上の必須要素の見落としに気づくことがある。
クラス設計や実装で気づいたことは必ずモデルにフィードバックする、フィードバックすることでモデルの正確性が向上する。さらに、そのモデルがクラスそしてコード品質を向上させる。
フィードバックのサイクルを回し続けることが設計品質向上の秘訣
機能性を左右するモデリング
機能性:ソフトウェア品質特性の一つ。顧客のニーズを満たす度合い。
機能性を上手く発揮するには、概念の正体や裏に隠れた重大な目的を見破る必要がある
機能性をイノベートする「深いモデル」
本質的に課題を解決し、機能性の核心に貢献するモデルをドメイン駆動生計では深いモデルという
深いモデルは一朝一夕に作られるものではなく、試行錯誤を積み重ね、改良をしていくことで発想が転換し
大きなブレイクスルーを伴って深いモデルを獲得できるものとされている
設計は一度やったら終わりでなく、日々繰り返しか改良していくことが重要
リファクタリング
リファクタリング:外から見た挙動を変えずに、構造を整理すること。
実際の現場におけるリファクタリングは難易度が高く、いくら注意深くリファクタリングしても、人間の注意力には限度があるためうっかりミスで挙動が変わり、バグを埋め込んでしまうかもしれない
では、安全にリファクタリングするにはどうすればよいのか
ユニットテストでリファクタリングのミスを防ぐ
手段の一つにユニットテストがある。リファクタリングにはユニットテストが必須と言われるくらい、リファクタリングはテストとセットで語られる。しかし、悪魔を呼び寄せる邪悪なコードには、テストコードが書かれていないことが多い。このようなコードをリファクタリングする際には、まずテストコードを用意しなくてはならない。
次からはテストのないプロダクションコードに対してテストコードを書き、リファクタする手順を記す
- コードの課題を整理する
どうリファクタリングするか、まずは課題およびあるべき構造を考える - あるべき構造のひな型クラスをある程度作る
- ひな型クラスに対してテストコードを書く
- テストを失敗させる
- テストを成功させるために最低限のコードを書く
- ひな型クラス内部でリファクタリング対象のコードを呼び出す
- テストが成功するよう、あるべき構造へロジックを少しずつリファクタしていく
この手順で行うとリファクタリング作業の途中、不注意でロジックの書き換えミスをしても、
テストの失敗ですぐに気が付けるので、安全にリファクタリングできる。
あやふやな仕様を理解するための分析方法
テストがないコードにテストを追加する、安全にリファクタリングする方法について話す
(詳しくはレガシーコード改善ガイドにあるらしい)
-
仕様化テスト
メソッド名から類推が困難・引数もそれぞれ何であるか分からないこのようにテストコードの書きようが
なく、安全にリファクタリングできそうにない時に活躍するのが「仕様化テスト」
仕様化テストはメソッドの仕様を分析するための手法
分析したいメソッドのテストを書き、そのメソッドがどのような挙動を示すか明らかにする方法
実際には、仕様化テストだけですべての仕様を明らかにするのは難しい -
試行リファクタリング
実際のプロダクションでは複雑奇怪なものが多く、あるべき構造の類推が困難であり、
そんなコードは往々にして仕様が不明瞭。このような状況で有用な分析手法が「試行リファクタリング」
正式にリファクタリングするのではなく、ロジックの意味や構造を分析するためにお試しでリファクタリングするもの
手順を以下に示す
- 対象コードをリポジトリからチェックアウト
- テストコードを書かずにプロダクションコードをリファクタリングする
これにより、可読性があがり、仕様理解が進む、あるべき構造が見えてくる、デッドコードが見えてくる、
どのようにテストコードを書けばいいか見えてくるなどの利点がある
試行リファクタリングはあくまで試行なのでリポジトリへマージしてはいけない
IDEリファクタリング
IDEにも機械的に正確にリファクタリングできる機能がある。今回はIntellij IDEAのリファクタリング機能のうち、2つを紹介する
リネーム
クラスやメソッド、変数の名前を一度に全て、正確に変更するリファクタリング機能
メソッド抽出
ロジックの一部をメソッドとして切り出す機能
リファクタリングで注意すべきこと
-
機能追加とリファクタリングを同時にやらない
自分が今機能追加しようとしているのか、それともリファクタリングしているのか混乱してしまう。
また、リポジトリへのコミットも、機能追加とリファクタリングどっちのためのものか見分けがつかなくなる。バグが起きた際、どの変更によるものなのか分析を難しくしてしまう。 -
スモールステップで実施する
コミットは、どうリファクタリングしたか分かる単位でコミットする。例えばメソッド名の変更とロジックの移動を実施する場合はコミットを分けるなど。数回コミットしたらprを作るのがおすすめ。変更が大量にあるとコンフリクトする可能性があるし、リファクタしたコードにバグがあった際にロールバックが大変だから。 -
無駄な仕様は削除することも視野に
ソフトウェアの仕様は利益に貢献するように定められるが、ほとんど利益に貢献しなくなった仕様や、バグがある仕様、他の仕様と競合(または矛盾)している仕様も紛れ込む。このような無駄な仕様がある状況では、リファクタリングは上手くいかない。リファクタリング前に無駄な仕様がないか、仕様を棚卸するのも一考。無駄な仕様やコードを予め削除出来れば、より快適に、よりきれいにリファクタ出来る。
設計の意義と設計への向き合い方
設計とは課題を効率的に解決する仕組みづくり。ソフトウェアにおける設計とはソフトウェア品質特性の向上を促進するための仕組みを作ること。
設計しないと開発生産性が低下する
悪魔を呼び寄せるコード=変更容易性の低いコード
変更が困難で壊れやすいコード=レガシーコード
レガシーコードが蓄積している状態=技術的負債
変更容易性の設計をしないと、開発生産性が低下する。その要因が2つある
- バグを埋め込みやすくなる
- 可読性が低下する
木こりのジレンマ
ある木こりが、斧で一生懸命木を切っていました
通りかかった旅人が気を切る様子をしばらく眺めていましたが、
なかなか木が切れません。
よく見てみると、斧が刃こぼれしていたので、旅人は言いました。
「刃を研げば楽に切れますよ」
木こりは答えました。
「刃を研ぐ時間なんてない」
木を切る時間をロジックの実装時間、研ぐ時間を設計する時間に置き換える。すると、設計していないとロジックの変更やデバッグに多大な時間を消費してしまう。そして設計する余裕さえなくなってしまうジレンマに陥ってしまう
一生懸命仕事した感覚だけが残って生産性は悪いまま
開発生産性が悪いと、新機能をなかなかリリースできなくなり、収益も出せなくなる。成果を出せない体質になっていく。
生産性が悪いまま、成果を出せずに頑張っても、それは一生懸命働いたとは言えない
本来一生懸命にやるべきなのは、成果を出しやすい構造を設計すること
国家規模の経済損失
レガシーコードによる生産性の低下がどれほどの損失になるか考える。
開発チームにはメンバーが20人、レガシーコードによる実装遅延が1人1日3時間発生すると仮定する。
単純計算で1日あたり60時間損失が発生している。これが一か月ともなると1200時間、一年で14400時間にもなる。この損失は、実際にはレガシーコードの量に比例するものではない、なぜなら複雑で混乱したロジックがあると、もっと混乱したロジックが作りこまれやすいから。経産省の資料によると技術的負債による損失額は 12 兆円になると試算。すさまじい損失額である。
ソフトウェアとエンジニアの成長性
ソフトウェアの価値や魅力をより高めるために仕様が追加・変更され、コードが変更される。
コードの変更容易性が高いほど、ソフトウェアの価値を素早く高められる。
つまり、ソフトウェアが素早く成長する。
変更容易性が悪化すると、ソフトウェアの成長性も悪化するがスキルの成長性まで悪化する。
エンジニアにとっての資産とは何か
エンジニアにとっての資産とは何か?貯蓄?高年収?
エンジニアにとっての本質的な資産とは技術力であると筆者は考える。
貯蓄はゼロでも、技術力さえあればどこでも稼いでいけるのがエンジニアであり、富を生み出す泉である。
しかし、レガシーコードは資産の蓄積、すなわち技術力の成長を妨げてしまう恐ろしい存在である。
その理由を以下に述べる。
レガシーコードは人に引きずられやすい
先輩社員や前任者が書いたコードがレガシーコードだと気づくのは困難で、「これが正だ」と感じてしまうために、レガシーコードと同じ書き方で、さらなるレガシーコードを量産してしまう。
つまり、レガシーコードは、レガシーコードの書き手を増やしてしまい、エンジニアを低スキルに陥れる。
レガシーコードは高品質設計を妨げる
中には、レガシーコードに気が付く人もいる。何とかテコ入れを試みるものの、アンバランスでトリッキーなレガシーコードにより、設計改善は非常に困難。納期などの都合により諦めてしまうケースも少なくない
つまり、高品質な設計実装の経験を積めなくなり、設計スキル向上を果たせなくなる
レガシーコードは開発工数を減少させる
レガシーコードは理解に多大な時間を要する。一方時間は有限である。したがって、本来もっと価値の高い仕事に充てられるべき時間が、目減りしてしまう。十分な経験を積めず、設計スキルに限らずスキルの向上を果たせなくなる。
課題を解決する
このような課題をどう解決すべきか整理する
課題が見えないとそもそも設計する意識が生まれない
課題を知覚できないと、そもそも設計仕様とする意識すら生まれない
知覚容易な課題と知覚困難な課題がある
どんな悪魔がいて、どんな悪さをするか知らなければ、技術的負債の存在を知覚することすらできない。
ソースコードの読解スキルと技術的負債の近くスキルは別物
理想形を知って初めて課題を知覚出来る
課題とは、理想と現状のギャップ。つまり、理想を知っていれば現状と比較でき、課題が分かる。理想的な設計と現状を比較することで、技術的負債が知覚可能になる。
変更容易性を比較できないジレンマ
技術的負債を低減する変更容易性設計の効果をどのように計測すればよいのか。
変更容易性は開発生産性から推し量ることは出来るが、正確に、すぐには比較できない
経時変化により効果がでるため、長い目で見たときにはじめて効果を観測できるものだからだ
コードの良し悪しを判断する指標
現状のソースコードの良し悪しを表す指標がある。コードの複雑さや可読性などの一連の品質指標をコードメトリクス、またはソフトウェアメトリクスという。
実行可能コードの行数
コメント行を除いた、実装可能なロジックを含む行数。行数が多いと多くのことをやりすぎている可能性がある。Rubyのコード解析ライブラリでは。デフォルトではコード行数の上限を超えると警告される。デフォルトでのコード行数の上限としてはメソッドが10行以内、クラスが100行以内である。
循環的複雑度
循環的複雑度(サイクロマティック複雑度):コード構造の複雑さを示す指標。
条件分岐やループ処理のネストが増えると複雑さは増大する。
循環的複雑度 | 複雑さの状態 | バグ混入率 |
---|---|---|
10以下 | 非常に良い構造 | 25% |
30以上 | 構造的なリスクあり | 40% |
50以上 | テスト不可能 | 70% |
75以上 | いかなる変更も誤修正を生む | 98% |
凝集度
凝集度が高いほど変更容易性が高く、良い構造。凝集度を測定するメトリクスにLCOMがあり、
計測ツールもある。
結合度
モジュール間の、依存度合いを表す指標。
モジュール粒度は凝集度と同様で、クラス粒度の結合度は、あるクラスが呼び出している、他のクラスの数量。依存しているクラスが多いほど、すなわち結合度が増大するほど多くの変更影響を考慮しなければならず、保守や仕様変更が困難になる。
結合度は解析ツールにより計測が可能。ツールなしでも、呼び出しクラスの数量を数えたり、クラス図を描画したりするだけでもある程度推量可能。結合度が高いと、単一責任ではない可能性が高いため、依存をもっと減らせないか、クラスを分割できないかを検討する。
チャンク
人間の短期記憶は一度に4±1個しか把握できないという説がある。この個数をマジカルナンバー4と呼ぶ。
記憶の個数の単位をチャンクという。クラス設計する際は、マジカルナンバー4を援用して、脳に優しい構造にするのを心がける。つまり、クラス内部で取り扱う概念は4±1個数になるように設計し、大きなクラスは小さなクラスに分割する。
設計対象と費用対効果
予算も時間も有限なため、設計やリファクタリングは無限には出来ない。プロダクト構造全体が粗悪であっても、設計的なテコ入れは一部しか出来ない現実がある。
仕様変更もないのに変更容易性を高めても、費用対効果が非常に低くなってしまう。
そのため、費用対効果の高い箇所を見定める能力も必要
パレートの法則
売り上げの8割は、全商品の2割が生み出しているように
「ほんの一部分が全体要素を生み出しているとする法則」、80:20の法則とも呼ばれる
重要な機能は顧客が着目するし、改善ニーズも多く上がる。そしてニーズを受けて頻繁に仕様変更が行われる。こうした重要かつ仕様変更が頻繁に発生する箇所を狙って設計改善すれば、変更コストが抑えられ、費用対効果が高くなる。
サービスの中心領域
サービスのウリになるビジネス領域を「コアドメイン」という。コアドメインは以下のように説明される
- システム内で最大の価値を付加すべき場所
- 価値があり重要で、費用対効果が最大の場所
- 競争優位性があり、差別化が図られ、ビジネス上優位に立つポイント
重点設計対象の選定にはビジネス知識が必要
ではコアドメインと呼べるビジネス領域とは何なのか。
重要な機能は高い頻度で仕様変更される傾向にある。
ドメイン駆動設計は、コアドメインの価値を継続的に高め、サービスを長期的に成長させる設計手法。
サービスの事業領域に関して深い知識を持っている人をドメインエキスパートと呼ぶ。
ドメインエキスパートと協力して、何がコアドメインか見定める必要があるという考えがドメイン駆動設計にある。
コアドメインとは何かを考えるとそれはサービスが解決したい顧客課題である。つまりコアドメインを見極めるためには、サービスの本質、ビジネス知識が必要になる。
構造の良し悪しだけに着目していると、ビジネス戦略上、設計が上手く働かなくなる。
成長性を高める最適な設計とビジネス知識は、切って離せない関係である。
時間を操る超能力者になろう
レガシーコードでいたずらに疲弊するか。ソフトウェアを素早く成長させられるようになるかは、設計者の腕次第。今の設計品質が未来の時間に直結しているのを意識できるようになりましょう。
時間に対しての注意力が高まると、デバッグの時間・コード読解の時間のような普段の開発で発生する無駄な時間が見えるようになる。現状が当たり前ではなく異常とすら思えるようになる。
エンジニアは設計のあるべき姿が分かるエンジニアとなり異常の原因を見破れる目を身につけなくてはいけない
設計を妨げる開発プロセスとの闘い
レガシーコードが書かれてしまうのは問題を抱えた開発プロセスが背景であることが多い。
問題は、スキル不足の他に心理的要因、コミュニケーション要因、組織的要因など様々
コミュニケーション
コミュニケーションが希薄だと設計品質に問題が生じる
すぐ隣の全く同じコードを書いている、お互いのロジックが上手くかみ合わずバグ化するといった現象がみられる。こうした現象はなぜ引き起こされるのか。それはお互いに何をやっているか分からないからである。それはなぜか、メンバー同士のコミュニケーションが希薄だからである。メンバー同士のコミュニケーションが問題だとバグが増大する傾向にある。
コンウェイの法則
コンウェイの法則は:システム構造がそれを設計する組織構造に似てくるという法則。
例えば開発部門が3チームに分かれていればモジュールも3個から構成されるシステムが出来上がるということ
なぜこのような現象が起きるのか、それはチーム内のコミュニケーションに比べてチーム外とのコミュニケーションが圧倒的に難しいからである。
この法則に基づくとあるべきシステム構造と組織構造に違いがあると、あるべきシステム構造を作り上げるのが困難になる。
そこで近年、逆コンウェイ作戦が考案されている。これは、ソフトウェアとしてあるべき構造を先に設計し、それからソフトウェア構造に最適な組織編制をする作戦
しかし、逆コンウェイ作戦を表面的になぞるだけでは上手くいかないと筆者は考える。チームがの問題を解決してもメンバー同士のコミュニケーションに課題があれば、嚙み合わず、本質的な構造課題の解決にはならないから
心理的安全性
チームメンバとの関係改善には、心理的安全性の向上が不可欠
心理的安全性とは自分が発言することを恥じたり、拒絶されるなど、不利益を被ることがないことをチームで共有されている心理状態のこと
コミュニケーションに課題があるときは、まず心理的安全性の向上を務める
設計
設計は重要な開発である。しかし、設計が行われない、設計がうまく働かない様々な罠が潜んでいる
早く終わらせたい心理が品質低下の罠
品質が良くないシステムを作るチームでは、そもそも設計の習慣がない。仕事が忙しいと、実装を早く終わらせたい気持ちが先走り、設計を疎かにして、動くコードをとにかく早く実装しがち。
環境によっては、それが正義のような雰囲気が作られる。しかし、それが罠。ほとんどのソフトウェアは1回作って終わりではなく、仕様変更が繰り返され、機能が拡張されていく。品質を無視した実装の繰り返しにより、粗悪なコードはどんどん蓄積されていく。
粗悪なコードはきれいなコードを書くより常に遅い
TDDとTDDを用いない実装どっちがはやいか比較実験したものがある。
TDDではプロダクションコードの他にテストコードを書かなくてはいけないため、用いない方が速く完成しそうではあるが、TDDの方が全体的に早いという結果になった。
この結果からも「動くコードをとにかく早くかけるのが正義」といった考えは危ない
クラス設計と実装のフィードバックサイクルを回す
仕様変更の際、最低でもメモ書き程度のクラス図を描くべき。
責務や凝集性などの観点から課題がないかをチームでざっくりレビューする。問題が無ければ実装する。
このサイクルを回すことで設計品質が高まる。
厳密に設計しすぎず、サイクルを回し続けるのがコツ
大がかりな仕様変更では、それなりにしっかりしたクラス設計が必要。一方で、厳密に設計しすぎるのはお勧めしない。どんなに設計しても、動作に必要な要素の見落としを実装してみて初めて気づくケースも多いから。また実装と乖離が生じた時の精神的ダメージもでかいから。
それにたった一度の設計では、良き構造は見いだせない。
パフォーマンスが落ちるからクラスは追加しないは正しいのか?
パフォーマンスに対してボトルネックになってる部分は計測しないと分からない。ボトルネックが分からないうちから高速に動作するコードをやみくもに書くことは、早すぎる最適化と呼ばれるアンチパターンである。
設計ルールを多数決で決めるとコード品質は最低になる
設計やコーディングルールを多数決で決定しようとすると不幸な結果になりがちである。
それは、どうしてもレベルの低い方に合わせて基準をまとめようとなってしまうからだ。
未熟なメンバーが多数を占めているチームでルールを多数決で決めようとすると何が起こるか?
ルールの意図が十分に理解されないまま反発が生まれ、ルールの採用が難しくなる。
改善提案は通りにくく、粗悪なルールが採用されたり、ルールそのものが策定されなくなる。
設計ルール作りのポイント
メンバー間の能力差が大きい場合は、多数決ではなく、設計スキルが高いメンバーが中心となってルールを作る。そしてチームリーダーの権限でルールの遵守を推進していく。
設計ルールにはそれぞれ、理由や意図を必ず明記する。
実装
コードへの向き合い方、考え方が変わると、実装への取り組み方が変わる。
割れ窓理論とボーイスカウトの規則
犯罪学に割れ窓理論という理論がある。以下の経過をたどって治安が悪化していくとする説である。
- 建物に割れた窓が一枚ある。
- 割れた窓が長く放置されていると、誰もきにかけていない象徴になる
- ほかの窓が割られる、ごみを捨てられるなど軽犯罪が起こり、次第に治安が悪化する
- さらに凶悪犯罪が起きるようになる
ソフトウェア開発でも同様で、複雑で秩序のないコードが放置されているとソフトウェア全体が無秩序になってしまう。
アメリカのボーイスカウトの規則には「キャンプ場を、自分が来た時よりもきれいにすること」がある。
これもプログラミングにあてはめることができ、自分が変更する前よりもきれいな状態にしてコミットする。
小さな積み重ねではあるが、小さな改善が小さな秩序の回復につながる。
既存のコードは信用せずに、冷静に正体を見破る
レガシーコードを撲滅するには既存のコードを一切信用しないぐらいの心構えが重要。
何を解決したいコードなのか、達成したい目的は何なのかを分析し、あるべき設計をゼロベースでくみ上げる必要がある。筆者はこれを正体を見破ると呼ぶ。正体を見破るに値りいくつか乗り越えるハードルがある
1つは、アンカリング効果である。これは最初に提示された数値や情報が基準となるため、その後の判断をゆがめてしまう認知バイアスである。2つ目は、ジョシュアツリーの法則と呼ばれる。これは名前を知って初めて存在を知覚出来るようになる。逆に名前を知らなくては知覚できない認知法則である。
これら二つのハードルを乗り越えて冷静に正体を見破り、正体を正しく表現した名前をクラスに付与しよう。
コーディング規約を利用しよう
コーディング規約とは、コードの可読性、保守向上や問題性のあるコードを未然に防ぐことを目的に、コーディングスタイルや命名規則などのルールを定めたものである。コーディング規約を遵守することで、コードの構造や命名に秩序が生まれ読みやすさが上がる。
命名規約
命名規約とは変数名やクラス名、メソッド名を決める際のルール。アッパーキャメルケースやロワーキャメルケーススネークケースなどがある。命名規約は言語や採用するコーディング規約によって異なる。
レビュー
レビュー時に注意すべき点や工夫の仕方をまとめる
コードレビューを仕組化しよう
レガシーコードが書かれる現場では、コードレビューの習慣がない場合が多い。とりあえず動作するだけの雑なコードが誰のチェックも通らず、次々にマージされていく。
品質が良くないのでバグが頻発することになる。PRのレビュアーにはコードを深く知っている人や設計に詳しい人をレビュアーとしてアサインしましょう。PR作成時のテンプレートテキストにはレビュー観点を盛り込み、ボーイスカウトの規則のチェック項目や設計ルールのリンクを張ったりするものよい
コードを設計視点でレビューしよう
コードレビューは機能要件として満足してるかではなく、設計的な妥当性を重点においてレビューすべき
。本書で挙げた設計観点と照らし合わせながらレビューする。
相手に敬意と礼儀を持つ
GoogleのChromiumプロジェクトのレビュー方針、尊敬に満ちたコードレビューを紹介する
すべきこと | 解説 |
---|---|
能力と善意を想定する | 開発者は十分な能力と善意を持つと想定する。ミスは情報不足に起因するものと考える |
会って話し合う | レビューツール上でのやりとりで意見がまとまらなければ、実際に話して意見を交換する |
理由を説明する | なぜ間違っているか、どういう変更が正しいかを説明する。ただ間違っているだけでは相手に伝わらない |
理由を聞く | 相手の意図が不明瞭な時は、遠慮せず変更理由を聞く。やり取りを記録することで将来的に変更の意図などが分かりやすくなる。また、より良い実装を考える機会にもなる |
終わりを見つける | 完璧に拘泥して徹底的なレビューをしようとすると、レビューされる側は疲弊する。「絶対に間違いないと保証する」ではなく、「よさそうです」でレビューを適切に終える |
適度な時間内に返信する | レビューをいつまでも放置しない。24時間以内に返信できなければ、いつまでに返信できるかをコメントで残す |
ポジティブに述べる | 全ての欠点を見つけてやるという気持ちで臨むのではなく、ポジティブな姿勢でレビューする。無理にほめる必要はないが、難しい仕事を引き受けてくれた人や、良い変更をしてくれた人に感謝する姿勢は大事 |
いけないこと | 解説 |
---|---|
人を辱めない | 相手は最善を尽くしていることを前提にする。「なぜ気づかなかった?」といった無意味なコメントをしない |
極端な言葉やネガティブな表現は使わない | 「まともな人間ならこうはしない」「ひどいアルゴリズムだ」などとネガティブな表現をレビュー時に使ってはいけない。人を脅して、思い通りに動かそうなどと考えてはいけない。人ではなく、コードについて議論する |
ツールの使用を思いとどまらせない | コードフォーマッターなどの自動化ツールを導入してくれたら、まずそのことに感謝する。ツールの利用の是非や好みを押し付けてはいけない |
自転車の置き場の議論をしない | どちらでもいいようなことについて、レビュー上で決着をつけようとしない。レビューの目的は勝ち負けではない |
定期的に改善タスクを棚卸すること
スケジュール的に対処が難しく、後回しになることがある。しかし、この後回しにした修正は放置されることが多い。タスク管理ツールに改善タスクとして積み上げ、定期的に棚卸して、確実に対処できるようにする。
チームの設計力を高める
設計に理解がないメンバーがいて上手くいかないことがある。その場合レビューがまともに機能しないケースも往々にしてある。チーム全体の設計力が不足している場合、設計力を高めていく活動が必要だが何をすればいいのか
影響力を持つレベルにまで仲間を集める
仕事のやり方をボトムアップで変えていくには周囲の協力が不可欠。自分以外を巻き込んでいかないといけない。まずは影響力を持てるだけの仲間を集めることが重要。クープマンの目標値によると影響力を持つために必要な目標値は全体の10.9%である。つまり20人であれば自分以外にもう1人仲間に出来ればいい
基本はスモールステップ
大きな変化は大きな抵抗を生む。毎日少しづつスモールステップで設計の知識を共有していく
実感が大事、手を動かす
知識や認識を共有出来たら、一緒にクラスを設計、実装してコードレビューする。
コードの見通しが良くなった、重複コードが減ったといった実感を仲間と共有してみる。
実際のプロダクションコードを使って改善実験を行い、シンプルで秩序のあるコードに改善することで設計のありがたみを実感できる。
りーだーやマネージャーに設計と費用対効果の話をする
変更容易性にテコ入れできず、低下の一途をたどってしまう組織的な問題は、そもそも開発リソースに変更容易性に関する設計コストが盛り込まれていないことが主たる要因。予算や計画を決めるメンバーであるリーダーやマネージャーに変更容易性に対する費用対効果を話し、問題解決のために設計業務があることを伝える。話す際は1対1ではなく仲間と一緒であることが望ましい。
設計責任者を立てる
開発メンバーの多くが設計品質を良くしていく意識があれば、品質向上のための様々な取り組みが自然となされる。しかし、そうではない場合、設計責任者を立てることをお勧めする。設計責任者は変更容易性の品質向上のため以下を推進する。
- 設計品質に関わるルールや開発プロセスの策定
- ルールの周知、教育
- リーダー・マネージャー層への共有
- 品質の可視化
- 設計品質の維持
ビジネスサイドから無理のある仕様変更が提案されることがある、その際は設計責任者として設計品質を守る責任がある。懸念のある仕様変更は、設計上どのような課題が生じるかについてビジネスサイドに伝える。
課題解消のための落としどころをお互いに模索する。時には仕様変更を断る勇気も必要