抽象化・カプセル化・情報隠蔽¶
一言で言うと¶
- 抽象化(abstraction): 使う側にとって重要なことだけを残し、詳細を削ぎ落とすこと。「何をするか」を残し「どうやるか」を消す。
- カプセル化(encapsulation): データとそれを操作するコードを1つにまとめること。
- 情報隠蔽(information hiding): 変わりそうな決定(内部表現・アルゴリズム)を外から見えなくすること。
3つは混同されがちですが、カプセル化はまとめる話、情報隠蔽は見せない話、抽象化は考えなくて済むようにする話です。
解決したい問題¶
内部の決定(データの持ち方、計算方法)が外部に見えていると、内部を変えただけで利用側が全部壊れる。利用側が知るべきことが多いと、使うたびに全詳細を理解する必要がある。
問題のある実装¶
// C++20(問題例)
struct Inventory {
// 内部表現がむき出し。「配列で持つ」という決定に全利用者が依存する
std::vector<int> itemIds;
std::vector<int> counts; // itemIds[i]の個数がcounts[i]、という暗黙の対応
};
// 利用側のあちこちにこういうコードが書かれる
void AddPotion(Inventory& inv) {
for (std::size_t i = 0; i < inv.itemIds.size(); ++i) {
if (inv.itemIds[i] == 42) { inv.counts[i]++; return; }
}
inv.itemIds.push_back(42);
inv.counts.push_back(1);
}
何が問題になるか¶
- 「2本のvectorの対応関係」という約束事を全利用者が守る必要がある。1か所でも崩すとデータ破損。
- 内部を
std::unordered_map<int,int>に変えたくなったら、全利用箇所を書き換え。 - 「上限チェック」「所持イベント通知」を追加したくても、追加し忘れる利用箇所が必ず出る。
改善した実装¶
// C++20
#include <unordered_map>
class Inventory {
public:
// 「何ができるか」だけを公開(抽象化されたインターフェース)
void Add(int itemId, int count = 1) {
counts_[itemId] += count; // ルール(上限、通知)を将来ここに足せる
}
bool Remove(int itemId, int count = 1) {
auto it = counts_.find(itemId);
if (it == counts_.end() || it->second < count) return false;
it->second -= count;
if (it->second == 0) counts_.erase(it);
return true;
}
int CountOf(int itemId) const {
auto it = counts_.find(itemId);
return it == counts_.end() ? 0 : it->second;
}
private:
std::unordered_map<int, int> counts_; // 内部表現は隠蔽。いつでも変えられる
};
- カプセル化: データ(counts_)と操作(Add/Remove)が1つのクラスにまとまった。
- 情報隠蔽: 「mapで持つ」という決定は private に隠れた。vector2本に戻しても利用側は無傷。
- 抽象化: 利用側は「追加できる・減らせる・数を聞ける」とだけ考えればよい。
何が改善されたか¶
- 内部表現の変更が Inventory 内で完結する。
- 整合性のルール(負の個数にならない等)を1か所で保証できる。
代わりに何を失ったか¶
- 直接いじるより手数が増えた。単純なデータの入れ物で十分な場面(純粋な設定データなど)では、publicなstructの方が正直で読みやすい。
- getter/setterを機械的に全フィールドに生やすと、隠蔽したつもりで何も隠せていない(内部表現と1:1のsetterは実質public)。公開するのは「操作の意図」(Add/Remove)であって「フィールドへのアクセス」ではありません。
「漏れる抽象」という限界¶
抽象化は完全にはなりません。例えば std::vector は「伸びる配列」という抽象ですが、伸びるときに要素がメモリ上で引っ越すという詳細は、イテレータ無効化という形で漏れてきます(→ STLコンテナ)。抽象の下で何が起きているかをいつでも覗けることが、C++プログラマの強みになります(第6部・第7部)。
ゲーム開発での例¶
- 物理エンジンの
Raycast(origin, dir)は、内部の空間分割構造(→ Spatial Partition)を完全に隠した抽象。 - 「HPを直接いじらせず
TakeDamage()を通す」ことで、無敵判定・ダメージ軽減・死亡判定のルールを1か所に保てる。
Unity/C# との対応¶
- C#の
private+ プロパティが同じ役割。public int hp;をインスペクタに出したいがために全部publicにするのは情報隠蔽の放棄です([SerializeField] privateを使う)。 - C# には
internal(アセンブリ内公開)があり、C++にはこれに直接対応するものがない(→ 第9部)。
Unreal Engine との対応¶
UPROPERTY(EditAnywhere)等でエディタ公開とコード上のアクセス制御を別々に指定できます。エディタに見せることとコードに見せることは別の決定です(→ 第10部)。
よくある誤解¶
- 「カプセル化 = privateにすること」— privateは手段の一つ。本質は変わりそうな決定を外に約束しないことです。
- 「抽象化 = 基底クラスを作ること」— 継承は抽象化の一手段にすぎません。関数を1つ切り出すのも立派な抽象化です。
使う場面 / 使わない場面¶
- 使う場面: 整合性ルールがあるデータ(HP、所持金、インベントリ)。内部表現を変える可能性がある部分。利用者が多い部分。
- 使わない場面: ただのデータの運び屋(POD的なstruct、設定値)。この場合 public メンバの方が意図が明確です(→ 構造体とクラス)。
関連項目¶
理解度チェック¶
- カプセル化と情報隠蔽の違いを一文ずつで言えますか。
- 「全フィールドにgetter/setterを生やす」ことが情報隠蔽にならないのはなぜですか。
- 抽象が「漏れる」例を1つ挙げてください。
演習¶
public float x, y; public float speed; だけの Missile struct に「速度上限」「移動は毎フレームUpdateで」というルールを追加することになりました。どのメンバをどう隠し、何を公開しますか。改善前後のコードを書いてください。
前: 依存関係と依存の方向 | カテゴリ目次 | 次: 継承・委譲・コンポジション