第1部 演習 — 設計の基礎¶
解答は各問の直後に折りたたみで置いています。まず自力で答えてから開いてください。
問1. 概念確認¶
次の記述の正誤を判定し、誤りなら訂正してください。
- 凝集度はモジュール間の関係を表す指標である。
- DRY原則は、同じ字面のコードを2度書くことを禁じている。
- リファクタリングとは、コードの振る舞いを改善することである。
- 継承は is-a、コンポジションは has-a の関係を表す。
解答
1. **誤り。** 凝集度はモジュール**内部**の関連の強さ。モジュール間は結合度([凝集度と結合度](03_cohesion_and_coupling.md))。 2. **誤り。** DRYが禁じるのは**知識**の重複。字面が同じでも変更理由が違うなら統合すべきでない([YAGNI・KISS・DRY](09_yagni_kiss_dry.md))。 3. **誤り。** リファクタリングは**振る舞いを変えずに**内部構造を改善すること。振る舞いの改善は機能追加・バグ修正であり、混ぜないことが原則([リファクタリング](10_tech_debt_and_refactoring.md))。 4. **正しい。** ただし「is-aが字面上成り立つ」だけでは不十分で、置換可能性([LSP](../02_solid/lsp.md))まで満たす必要がある。問2. コード読解(変更理由の数)¶
// C++20
class Player {
public:
void Update() {
if (Input::GetKey(Key::A)) x_ -= speed_;
if (Input::GetKey(Key::D)) x_ += speed_;
if (hp_ <= 0) { PlayDeathAnim(); SaveScoreToFile(); }
hpBarWidth_ = 200.0f * hp_ / maxHp_;
}
// メンバ定義は省略
};
このUpdateに混ざっている「変更理由」を列挙してください。
解答
少なくとも4つ: (1) キー割り当て・入力方式の変更(入力)、(2) 移動仕様の変更(操作ロジック)、(3) 死亡時の演出・記録の変更(演出/永続化 — さらに分ければ2つ)、(4) HPバーの見た目の変更(UI)。SaveScoreToFile はファイル形式という別の変更理由も抱えている。**別解**: 分類の粒度は一意ではない。「入力と移動を操作系として1つに数える」のも、当面一緒に変わるなら妥当。重要なのは列挙できることと、分割コストに見合うかの判断([責務](02_responsibility_and_separation.md))。問3. 問題のあるコードの改善¶
問2のコードを、変更理由ごとに分ける改善案の骨格(クラス名と持つ関数だけで可)を書いてください。分けすぎない案も1つ作ってください。
解答
**分割案**: `PlayerInput`(意図の生成: MoveLeft/MoveRight)、`PlayerMovement`(座標更新)、`PlayerHealth`(hpと死亡通知イベント)、UI・保存は `PlayerHealth` のイベント購読側へ。 **分けすぎない案**: `Player`(入力+移動+HP)はそのまま、**UIと保存だけ**イベント購読に出す。入力と移動は当面同時に変わるので急いで分けない。 **トレードオフ**: 分割案はテスト容易・変更が局所化するが、ファイル4つ+イベント機構が必要。小規模なら後者で十分。「全部分ける」だけが正解ではない点が本問の狙い([YAGNI](09_yagni_kiss_dry.md))。問4. 設計比較¶
「敵がプレイヤーを発見したらBGMを戦闘曲に変える」機能の実装として、次の2案の利点・欠点を比較してください。
- 案A:
EnemyがBgmManager::Instance().Play("battle")を直接呼ぶ - 案B:
EnemyがPlayerSpottedイベントを発行し、BGM側が購読する
解答
**案A** 利点: 追いやすい(発見処理を読めばBGMが変わると分かる)、実装が数分。欠点: EnemyがBGM管理に依存([依存の方向](04_dependency.md)が悪い — ゲームロジックが演出に依存)、BGM側の仕様変更(発見者数で曲を変える等)のたびにEnemyを触る、Enemyのテストに音システムが要る。 **案B** 利点: Enemyは戦闘ロジックに集中、購読側(BGM・UI・実績)を無限に足せる。欠点: 「なぜ曲が変わった?」の追跡が困難になる、イベント機構が必要。 **判断**: 発見イベントに反応するものがBGMだけで今後も増えないなら案Aで十分。既にUI表示や実績も反応したがっているなら案B([Observer](../03_design_patterns/observer.md))。問5. デバッグ問題¶
あるプロジェクトで「HPバーの表示を変更したら、敵のAIが動かなくなった」という報告がありました。設計の観点から、最も疑わしい構造上の問題は何ですか。
解答
UI(HPバー)とAIが**結合している**こと。典型的には (1) 神クラスがUI更新とAI更新を1つの関数で行っており、UI変更時にAI呼び出しを誤って壊した、(2) AIがUIオブジェクトの状態(表示中フラグ等)を参照している、(3) 共有のグローバル状態(Singleton)を両者が読み書きしている。いずれも「無関係なはずの変更が波及する」高結合の症状([凝集度と結合度](03_cohesion_and_coupling.md))。調査の起点はHPバー変更のdiffと、そのコードを参照しているものの逆検索。問6. 説明問題¶
「カプセル化と情報隠蔽の違い」を、コードを1行も使わずに後輩に説明する文章を3文以内で書いてください。
解答例
カプセル化は「データとそれを扱う操作を1つの箱にまとめる」こと。情報隠蔽は「箱の中の、将来変わりそうな決め事を外から見えなくする」こと。まとめても全部publicなら隠蔽はしていないし、隠蔽はまとめ方の良し悪しとは別の判断——という関係です([抽象化・カプセル化・情報隠蔽](05_abstraction_encapsulation.md))。問7. 小規模実装問題¶
「コイン・回復薬・鍵」の3種類が拾えるゲームを想定し、次の2通りで実装してください(コンパイルが通る最小構成)。
- enum + switch 版
- インターフェース版(
IPickup)
その後、両方に「拾うと10秒間移動速度2倍のブーツ(効果が時間経過で切れる)」を追加し、変更の広がり方の違いを短くまとめてください。
解答の要点
実装例は [samples/design_basics_pickup.cpp](../samples/design_basics_pickup.cpp) にあります(switch版とインターフェース版の両方)。 まとめの要点: switch版は「拾った瞬間の処理」に**時間経過**という新しい概念が入った時点で破綻し始める(効果の管理場所がswitchの外に必要になる)。インターフェース版は `IPickup` が `OnPickedUp` しか持たないと同じ問題に当たる——つまり**どちらの方式でも「継続効果」は拾得処理と別の設計(効果システム)が必要**、が本質。方式の優劣より、仕様の変化が設計の前提を壊す例として覚えるとよい([ケーススタディ: アイテム効果](../11_case_studies/12_item_effects.md))。前: 技術的負債・リファクタリング | カテゴリ目次 | 第2部: SOLID原則へ