この記事の対象読者
-
Python や JavaScript で
classを使っているが、カプセル化を「とりあえず private にして getter/setter を書くこと」だと思っている人 -
__init__で属性を定義したあと、全属性に対して機械的に getter/setter を生やしている人 - 「カプセル化ってデータを隠すことでしょ?」と聞かれて、それ以上説明できない人
- 前回のオブジェクト指向の記事を読んで、カプセル化をもう一段深く理解したい人
この記事で得られること
- カプセル化の本質が「情報隠蔽」ではなく「責任境界の設計」であると理解できる
- getter/setter の濫用がなぜアンチパターンなのか、銀行システムの比喩で腹落ちする
- Python の
_/__/@propertyの使い分けが明確になる - JavaScript の
#プライベートフィールドとクロージャによるカプセル化を実装できる - 実務で遭遇する5つのカプセル化違反パターンと、その修正方法がわかる
この記事で扱わないこと
- オブジェクト指向の4大原則全体の解説 → 前回の記事を参照
- Java / C# / C++ の
private/protected/public修飾子の詳細 - デザインパターンとの関連(Facade / Mediator 等)は別記事で扱う予定
1. 「隠す」だけなら猿でもできる
筆者がカプセル化を初めて学んだとき、こう理解した。
「属性を private にして、getter と setter を書く。以上。」
これが間違いだったと気づくのに、恥ずかしながらかなりの時間がかかった。
何が問題なのか。全属性に getter/setter を書いた時点で、「隠す」と「全公開する」を同時にやっている のだ。玄関に鍵をかけたのに、鍵のコピーを玄関マットの下に置いているようなものだ。
本記事では、カプセル化の本質を 「銀行」 という一貫した比喩で解き明かしていく。なぜ銀行なのか。銀行は「他人のお金を預かり、決められたルールでのみ操作を許可する」という、カプセル化そのものの仕組みで成り立っているからだ。
ATMの裏側でどんなシステムが動いているか、利用者は知らない。知る必要もない。ただ「引き出し」「預け入れ」「残高照会」という窓口を通じて操作するだけだ。この構造がそのまま、良いカプセル化の設計原則になる。
2. 銀行で理解するカプセル化の3層構造
カプセル化は単一の概念ではない。実は 3つのレイヤー で構成されている。これを銀行に対応づけて整理しよう。
| レイヤー | 銀行での対応 | OOPでの意味 |
|---|---|---|
| データの保護 | 金庫の中のお金を勝手に触らせない | 属性への直接アクセスを制限する |
| 操作の窓口化 | ATM・窓口という決められた手段でのみ操作 | メソッドを通じてのみ状態を変更する |
| 内部実装の隠蔽 | ATM利用者は裏側のシステムを知らない | 呼び出し側がクラスの内部構造に依存しない |
多くの入門書は第1レイヤー「データの保護」しか教えない。しかし本当に重要なのは第2・第3レイヤーだ。順番に見ていこう。
レイヤー1: データの保護 ─ 金庫を勝手に開けさせない
最も基本的な層。銀行の金庫室に一般客が入れないのと同じで、オブジェクトの内部データに外部から直接アクセスさせない。
class BankAccount:
def __init__(self, owner: str, initial_balance: int):
self.owner = owner
self.__balance = initial_balance # 金庫の中身(外部アクセス禁止)
# 直接金庫を開けようとすると...
account = BankAccount("田中", 1000000)
# print(account.__balance) # AttributeError! 金庫は開かない
ここまでは誰でも知っている。問題は次だ。
レイヤー2: 操作の窓口化 ─ ATMのボタンだけを提供する
データを隠しただけでは意味がない。「どういう操作を、どういう条件で許可するか」 を設計するのが本番だ。
銀行のATMを思い浮かべてほしい。ATMは「引き出し」ボタンを押したからといって、無条件にお金を出すわけではない。暗証番号の確認、残高チェック、1日の引き出し上限チェックなど、ビジネスルールに基づいたバリデーションを行う。
class BankAccount:
"""銀行口座: カプセル化の3層構造を実装"""
DAILY_WITHDRAWAL_LIMIT = 500000 # 1日の引き出し上限
def __init__(self, owner: str, initial_balance: int):
self.owner = owner
self.__balance = initial_balance
self.__transaction_history: list[dict] = []
self.__daily_withdrawn = 0
# --- ATMのボタン(パブリックメソッド = 操作の窓口) ---
def deposit(self, amount: int) -> str:
"""預け入れ窓口"""
if amount <= 0:
return "エラー: 0円以下は預け入れできません。"
self.__balance += amount
self.__record_transaction("deposit", amount)
return f"預入完了: {amount:,}円 → 残高: {self.__balance:,}円"
def withdraw(self, amount: int) -> str:
"""引き出し窓口"""
if amount <= 0:
return "エラー: 0円以下は引き出せません。"
if amount > self.__balance:
return f"エラー: 残高不足です(残高: {self.__balance:,}円)"
if self.__daily_withdrawn + amount > self.DAILY_WITHDRAWAL_LIMIT:
remaining = self.DAILY_WITHDRAWAL_LIMIT - self.__daily_withdrawn
return f"エラー: 1日の引き出し上限を超えます(残り: {remaining:,}円)"
self.__balance -= amount
self.__daily_withdrawn += amount
self.__record_transaction("withdraw", amount)
return f"引出完了: {amount:,}円 → 残高: {self.__balance:,}円"
def get_balance(self) -> str:
"""残高照会窓口"""
return f"{self.owner}様の残高: {self.__balance:,}円"
def get_statement(self, last_n: int = 5) -> str:
"""取引明細窓口(直近N件)"""
if not self.__transaction_history:
return "取引履歴はありません。"
recent = self.__transaction_history[-last_n:]
lines = [f" {t['type']:>8} | {t['amount']:>10,}円" for t in recent]
return f"【取引明細(直近{len(recent)}件)】\n" + "\n".join(lines)
# --- 金庫の裏側(プライベートメソッド = 内部実装) ---
def __record_transaction(self, tx_type: str, amount: int):
"""取引記録の内部処理(利用者には見えない)"""
self.__transaction_history.append({
"type": tx_type,
"amount": amount,
})
# 利用例
account = BankAccount("田中", 1000000)
print(account.deposit(200000))
# 預入完了: 200,000円 → 残高: 1,200,000円
print(account.withdraw(300000))
# 引出完了: 300,000円 → 残高: 900,000円
print(account.withdraw(300000))
# 引出完了: 300,000円 → 残高: 600,000円
print(account.withdraw(100000))
# エラー: 1日の引き出し上限を超えます(残り: 0円) ← ちゃんと守られる
print(account.get_statement())
ここが最重要ポイントだ。withdraw() メソッドは単なる getter/setter ではない。残高チェック、上限チェック、取引記録という ビジネスルールを内包している。もし __balance を直接公開していたら、これらのルールをすべてバイパスできてしまう。ATMのボタンを押す代わりに、金庫の壁を直接ぶち破るようなものだ。
レイヤー3: 内部実装の隠蔽 ─ ATMの裏側は知らなくていい
3番目のレイヤーは、利用者が内部構造に依存しないようにする仕組みだ。
上のコードで __transaction_history がリストで実装されているか、データベースに保存されているか、利用者は知らない。将来的に保存方式を変更しても、get_statement() の戻り値さえ変わらなければ、利用側のコードは一切変更不要だ。
利用者は左側の4つのボタンだけを操作する。右側の内部構造には一切触れない。この境界線が「カプセル化」の実体だ。
3. getter/setter がアンチパターンになる瞬間
ここまで読んで、鋭い読者はこう思ったかもしれない。
「じゃあ
get_balance()は getter じゃないの?」
良い質問だ。違いを明確にしよう。
「良い」窓口と「悪い」getter/setter
| 種別 | 銀行での対応 | コードでの例 | 判定 |
|---|---|---|---|
| 残高照会 | ATMで残高を確認する |
get_balance() → 書式付き文字列を返す |
窓口として適切 |
| 残高の直接取得 | 金庫を開けて札束を数える |
get_balance_raw() → int をそのまま返す |
グレーゾーン |
| 残高の直接設定 | 金庫に勝手に札束を入れる |
set_balance(value) → 無条件に書き換える |
アンチパターン |
決定的な違いは ビジネスルールを通すかどうか だ。
# BAD: 全属性にgetter/setterを機械的に生やす(金庫の鍵を全員に配る)
class BrokenBankAccount:
def __init__(self, owner: str, balance: int):
self.__owner = owner
self.__balance = balance
def get_owner(self):
return self.__owner
def set_owner(self, value):
self.__owner = value
def get_balance(self):
return self.__balance
def set_balance(self, value): # これが致命的
self.__balance = value
# 何が起きるか
account = BrokenBankAccount("田中", 1000000)
account.set_balance(-999999) # 残高がマイナスに。銀行破綻。...orz
account.set_owner("") # 名義人が消えた。口座の持ち主不明。
set_balance() が無条件に値を受け入れている時点で、カプセル化は完全に崩壊している。これは「金庫にドアを付けました。でも鍵はかけません」と言っているのと同じだ。private にした意味がゼロになる。
では、どう書くべきか?
原則は明快だ。「操作」を公開し、「データ」は公開しない。
# GOOD: 操作を窓口として提供する
class SolidBankAccount:
def __init__(self, owner: str, initial_balance: int):
if not owner.strip():
raise ValueError("口座名義は必須です")
if initial_balance < 0:
raise ValueError("初期残高は0以上にしてください")
self.__owner = owner
self.__balance = initial_balance
# 残高は「照会」として提供する(フォーマット済み)
def get_balance_display(self) -> str:
return f"{self.__owner}様: {self.__balance:,}円"
# 残高変更は「預入」「引出」という業務操作を通じてのみ許可
def deposit(self, amount: int) -> str:
if amount <= 0:
raise ValueError("預入額は1円以上にしてください")
self.__balance += amount
return f"預入完了: 残高 {self.__balance:,}円"
def withdraw(self, amount: int) -> str:
if amount <= 0:
raise ValueError("引出額は1円以上にしてください")
if amount > self.__balance:
raise ValueError(f"残高不足(残高: {self.__balance:,}円)")
self.__balance -= amount
return f"引出完了: 残高 {self.__balance:,}円"
# 名義変更は「改姓届」という業務フローで提供
def change_owner_name(self, new_name: str, reason: str) -> str:
if not new_name.strip():
raise ValueError("新しい名義は必須です")
old_name = self.__owner
self.__owner = new_name
return f"名義変更完了: {old_name} → {new_name}(理由: {reason})"
こちらのコードでは set_owner() や set_balance() は存在しない。代わりに change_owner_name() という 業務上意味のある操作名 を持つメソッドがある。理由の記録も求める。銀行の窓口で「名義変更届」を書くのと同じ流れだ。
4. Python のアクセス制御 ─ _ と __ と @property
Python にはアクセス制御の仕組みが3段階ある。銀行のセキュリティレベルに対応づけて整理しよう。
| Python記法 | セキュリティレベル | 銀行での対応 | 実際の挙動 |
|---|---|---|---|
self.name |
制限なし | 銀行のロビー。誰でも入れる | どこからでもアクセス可能 |
self._name |
慣習的制限 | 行員専用エリア。入れるが「関係者以外立入禁止」の表示 | アクセス可能だが「触るな」の意思表示 |
self.__name |
名前マングリング | 金庫室。物理的にドアが別ルートに変わる |
_ClassName__name に変換される |
_ シングルアンダースコア ── 紳士協定
class InternalSystem:
def __init__(self):
self._cache = {} # 「内部用だから外から触らないでね」という意思表示
def get_data(self, key: str):
if key in self._cache:
return self._cache[key]
# ... 実際のデータ取得処理
_cache は外部からアクセスできてしまうが、「これは内部実装の一部です」という設計者の意図を示している。Python コミュニティでは広く尊重される慣習だ。
__ ダブルアンダースコア ── 名前マングリング
class VaultSystem:
def __init__(self):
self.__secret_key = "ultra-secret-123"
vault = VaultSystem()
# print(vault.__secret_key) # AttributeError!
# print(vault._VaultSystem__secret_key) # アクセスできてしまう(裏口)
Python の __ は「完全な private」ではない。_ClassName__attribute という裏口が存在する。これは Python の設計哲学 "We are all consenting adults here" に基づいている。「大人同士なんだから、ルールを守ろうね」という紳士協定の延長だ。Java や C# のような言語レベルの強制はない。
@property ── 最も Pythonic なカプセル化
Python でカプセル化を実現する最も推奨される方法が @property デコレータだ。銀行で言えば、ATMの画面に表示される情報を、裏側で加工してから見せる仕組み に相当する。
class BankAccount:
def __init__(self, owner: str, initial_balance: int):
self._owner = owner
self._balance = initial_balance
@property
def balance(self) -> str:
"""残高照会(読み取り専用 + フォーマット済み)"""
return f"{self._balance:,}円"
@property
def balance_raw(self) -> int:
"""残高の数値(内部計算用)"""
return self._balance
@property
def owner(self) -> str:
"""口座名義(読み取り専用)"""
return self._owner
@owner.setter
def owner(self, new_name: str):
"""名義変更(バリデーション付き)"""
if not new_name.strip():
raise ValueError("名義は空にできません")
if len(new_name) > 50:
raise ValueError("名義は50文字以内にしてください")
self._owner = new_name
account = BankAccount("田中太郎", 1500000)
# 属性アクセスの見た目で、裏側ではバリデーションが走る
print(account.balance) # 1,500,000円(フォーマット済み)
print(account.balance_raw) # 1500000(計算用の生データ)
print(account.owner) # 田中太郎
account.owner = "田中花子" # setter経由 → バリデーション通過
# account.owner = "" # ValueError! 空文字はNG
# account.balance = 9999999 # AttributeError! setterが未定義 → 書き換え不可
@property の威力は インターフェースを変えずに内部実装を変更できる 点にある。最初は単純な属性だったものを、後からバリデーション付きの @property に変更しても、呼び出し側のコードは account.owner のまま一切変わらない。
5. JavaScript のカプセル化 ─ # とクロージャ
JavaScript にも2つのカプセル化手法がある。
手法1: # プライベートフィールド(ES2022+、推奨)
class BankAccount {
// プライベートフィールド(言語レベルで完全にprivate)
#balance;
#transactionHistory = [];
constructor(owner, initialBalance) {
this.owner = owner; // publicフィールド(ロビー)
this.#balance = initialBalance; // privateフィールド(金庫)
}
// 公開窓口
deposit(amount) {
if (amount <= 0) throw new Error("預入額は1円以上");
this.#balance += amount;
this.#recordTx("deposit", amount);
return `預入完了: 残高 ${this.#balance.toLocaleString()}円`;
}
withdraw(amount) {
if (amount <= 0) throw new Error("引出額は1円以上");
if (amount > this.#balance) throw new Error("残高不足");
this.#balance -= amount;
this.#recordTx("withdraw", amount);
return `引出完了: 残高 ${this.#balance.toLocaleString()}円`;
}
get balance() {
return `${this.#balance.toLocaleString()}円`;
}
// プライベートメソッド(金庫の裏側)
#recordTx(type, amount) {
this.#transactionHistory.push({ type, amount, date: new Date() });
}
}
const account = new BankAccount("田中", 1000000);
console.log(account.deposit(500000));
// console.log(account.#balance); // SyntaxError! 完全にprivate
JavaScript の # プレフィックスは Python の __ と違い、言語仕様レベルで完全なプライベートだ。通常のコードからの裏口は存在しない。ただし Chrome DevTools の Console に限り、デバッグ支援のため # フィールドへのアクセスが特別に許可されている(ECMAScript 仕様の意図的な緩和)。Firefox / Safari の Console ではこの緩和はない。実行中のアプリケーションコードからは一切アクセスできないため、Python の名前マングリング( _ClassName__attr で突破可能)より厳密なカプセル化と言える。
手法2: クロージャによるカプセル化(レガシー環境向け)
ES2022以前の環境や、関数型アプローチを好む場合はクロージャで実現できる。
function createBankAccount(owner, initialBalance) {
// ここで宣言された変数は外部から一切アクセスできない
let balance = initialBalance;
const history = [];
return {
deposit(amount) {
if (amount <= 0) throw new Error("預入額は1円以上");
balance += amount;
history.push({ type: "deposit", amount });
return `預入完了: 残高 ${balance.toLocaleString()}円`;
},
withdraw(amount) {
if (amount <= 0) throw new Error("引出額は1円以上");
if (amount > balance) throw new Error("残高不足");
balance -= amount;
history.push({ type: "withdraw", amount });
return `引出完了: 残高 ${balance.toLocaleString()}円`;
},
getBalance() {
return `${owner}様: ${balance.toLocaleString()}円`;
},
};
}
const account = createBankAccount("田中", 1000000);
console.log(account.deposit(200000));
// balance にアクセスする手段は一切ない
クロージャ方式は class を使わないが、カプセル化の本質 ── データを閉じ込めて、窓口だけを公開する ── を最もピュアに体現している。
6. 実務で遭遇するカプセル化違反5パターン
理論はわかった。では実務で何に気をつければいいのか。筆者が実際に踏んだ、あるいはコードレビューで見つけた5つの違反パターンを紹介する。
| # | 違反パターン | 銀行で言うと | 症状 | 修正方針 |
|---|---|---|---|---|
| 1 | 全属性 getter/setter | 金庫の鍵を全員に配る | private にした意味が消滅 | 業務操作メソッドに置き換え |
| 2 | 内部コレクションの参照漏れ | 金庫の中身リストを外に渡す | 外部から中身を書き換えられる | コピーまたはイミュータブルで返す |
| 3 | Tell, Don't Ask 違反 | 客が残高を聞いて自分で計算する | ロジックが呼び出し側に漏れる | 判定ロジックをクラス内に移動 |
| 4 | Feature Envy | 他の部署の仕事を勝手にやる | 別クラスのデータに頻繁にアクセス | メソッドを適切なクラスに移動 |
| 5 | Law of Demeter 違反 | 取引先の取引先に直接電話する | 連鎖的なドットアクセス | 直接の関係者にのみ依頼する |
パターン2: 内部コレクションの参照漏れ
これは見落としやすい。銀行が「取引明細の原本」をそのまま渡してしまう状態だ。
# BAD: 内部リストの参照をそのまま返している
class LeakyAccount:
def __init__(self):
self.__transactions = [
{"type": "deposit", "amount": 100000},
{"type": "withdraw", "amount": 30000},
]
def get_transactions(self):
return self.__transactions # 原本の参照を渡してしまう
account = LeakyAccount()
txns = account.get_transactions()
txns.clear() # 外部から内部データが消し飛ぶ!草
print(account.get_transactions()) # [] ← 取引履歴が全滅
# GOOD: コピーを返す(原本は金庫に保管)
class SafeAccount:
def __init__(self):
self.__transactions = [
{"type": "deposit", "amount": 100000},
{"type": "withdraw", "amount": 30000},
]
def get_transactions(self) -> list[dict]:
return [tx.copy() for tx in self.__transactions] # 深いコピーで返す
account = SafeAccount()
txns = account.get_transactions()
txns.clear() # コピーを消しただけ
print(account.get_transactions()) # 元データは無傷
パターン3: Tell, Don't Ask 違反
「聞くな、命じろ」という原則。銀行で例えると、客がATMに「残高いくら?」と聞いてから「じゃあ引き出せるな」と自分で判断する のは変だ。ATMに「5万円引き出して」と命じれば、ATM側で残高チェックから引き出しまで全部やってくれる。
# BAD: 呼び出し側がデータを取得して自分で判定(Ask)
def process_payment_bad(account: BankAccount, amount: int):
if account.balance_raw >= amount: # 残高を聞いて
account.force_withdraw(amount) # 自分で判断して引き出す
else:
print("残高不足")
# GOOD: オブジェクトに操作を命じる(Tell)
def process_payment_good(account: BankAccount, amount: int):
try:
result = account.withdraw(amount) # 「引き出して」と命じるだけ
print(result)
except ValueError as e:
print(f"決済失敗: {e}")
「Tell, Don't Ask」を徹底すると、ビジネスロジックが自然とそのデータを持つクラスに集約される。結果として、ロジックの重複や不整合が激減する。「データを持っているやつが判断しろ」── これが原則だ。
パターン5: デメテルの法則違反
# BAD: 取引先の取引先に直接電話する
company.get_department("accounting").get_manager().approve(invoice)
# GOOD: 直接の関係者に依頼する
company.approve_invoice(invoice)
ドットが2つ以上連鎖していたら、デメテルの法則違反を疑おう。「友達の友達は他人」だ。他人の内部事情に手を突っ込んではいけない。
7. カプセル化のレベル別チェックリスト
自分のコードがどの段階にあるか、レベル別にチェックしてみよう。
| レベル | 状態 | 目安 |
|---|---|---|
| Lv.1 |
private + 全 getter/setter |
入門書を読んだ直後 |
| Lv.2 | setter にバリデーション追加 | 「不正な値が入るとまずい」と気づいた |
| Lv.3 | getter/setter 廃止、操作ベースのメソッドに移行 | カプセル化の本質を理解した |
| Lv.4 | 内部変更が外部に波及しない、Tell Don't Ask 徹底 | 設計上級者 |
8. ユースケース別 ─ カプセル化が効く場面
ユースケース1: 設定管理クラス
アプリケーションの設定値を管理するクラス。設定値を勝手に変えられると障害に直結するため、カプセル化が必須だ。
class AppConfig:
"""アプリケーション設定: 銀行の融資審査基準のように厳格に管理する"""
VALID_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
def __init__(self, db_url: str, log_level: str = "INFO"):
self.__db_url = db_url
self.__log_level = self.__validate_log_level(log_level)
@property
def db_url(self) -> str:
"""DB接続先(読み取り専用。実行中の変更は許可しない)"""
return self.__db_url
@property
def log_level(self) -> str:
return self.__log_level
@log_level.setter
def log_level(self, value: str):
self.__log_level = self.__validate_log_level(value)
def __validate_log_level(self, level: str) -> str:
level = level.upper()
if level not in self.VALID_LOG_LEVELS:
raise ValueError(
f"無効なログレベル: {level}。"
f"有効値: {', '.join(sorted(self.VALID_LOG_LEVELS))}"
)
return level
config = AppConfig("postgresql://localhost/mydb", "INFO")
config.log_level = "DEBUG" # OK: バリデーション通過
# config.log_level = "VERBOSE" # ValueError! 無効なログレベル
# config.db_url = "sqlite://" # AttributeError! 読み取り専用
ユースケース2: ゲームキャラクターのHP管理
class Character:
"""RPGキャラクター: HPは銀行残高と同じく厳格に管理する"""
def __init__(self, name: str, max_hp: int):
self.name = name
self.__max_hp = max_hp
self.__current_hp = max_hp
@property
def hp(self) -> str:
bar_length = 20
filled = int(self.__current_hp / self.__max_hp * bar_length)
bar = "█" * filled + "░" * (bar_length - filled)
return f"{self.name} [{bar}] {self.__current_hp}/{self.__max_hp}"
def take_damage(self, damage: int) -> str:
"""ダメージを受ける(0未満にはならない)"""
actual = min(damage, self.__current_hp)
self.__current_hp -= actual
status = "戦闘不能..." if self.__current_hp == 0 else ""
return f"{self.name}は{actual}ダメージを受けた! {status}"
def heal(self, amount: int) -> str:
"""回復する(最大HPを超えない)"""
before = self.__current_hp
self.__current_hp = min(self.__current_hp + amount, self.__max_hp)
healed = self.__current_hp - before
return f"{self.name}はHPが{healed}回復した!"
hero = Character("勇者", 100)
print(hero.hp) # 勇者 [████████████████████] 100/100
print(hero.take_damage(35)) # 勇者は35ダメージを受けた!
print(hero.hp) # 勇者 [█████████████░░░░░░░] 65/100
print(hero.heal(50)) # 勇者はHPが35回復した!(上限で止まる)
print(hero.hp) # 勇者 [████████████████████] 100/100
HP を直接 hero.hp = 999 と書き換えられないからこそ、ゲームバランスが成り立つ。カプセル化は「ルールの強制力」 だ。
ユースケース3: カプセル化を使わないほうがいい場面
| 場面 | 理由 | 推奨手法 |
|---|---|---|
| DTO(データ転送オブジェクト) | 純粋なデータの入れ物。ロジックを持たない |
dataclass(frozen=True) や NamedTuple
|
| 設定値の構造体 | 読み取り専用で十分。操作が不要 |
@dataclass でシンプルに |
| 50行以下のスクリプト | オーバーエンジニアリング | 辞書や関数で十分 |
from dataclasses import dataclass
# DTO: カプセル化不要。データの入れ物として使う
@dataclass(frozen=True)
class TransferRequest:
from_account: str
to_account: str
amount: int
9. 学習ロードマップ ─ カプセル化の先にあるもの
カプセル化を理解した読者が、次に進むべきステップを示す。
| ステップ | 学ぶべきこと | カプセル化との関連 |
|---|---|---|
| 次 | SOLID原則(単一責任、インターフェース分離) | 「何を隠すか」の判断基準が明確になる |
| 次 | Facade パターン | 複雑なサブシステムを1つの窓口で包む ── カプセル化の拡大適用 |
| 次 | Repository パターン | データアクセス層のカプセル化。DB の内部構造を隠蔽する |
| 上級 | 境界付けられたコンテキスト(DDD) | システム全体をカプセル化された領域に分割する設計手法 |
カプセル化は「クラスの中の話」で終わらない。モジュール、パッケージ、マイクロサービスのレベルでも同じ原則が適用される。銀行の比喩で言えば、1つの支店の内部設計から、銀行グループ全体の組織設計にまでスケールする考え方だ。
10. 環境別サンプル
本記事のコードは Python 3.10 以上、JavaScript は ES2022 以上で動作する。
クリックで環境セットアップを展開
ローカル環境(Python):
# 標準ライブラリのみ使用。追加パッケージ不要。
# Python 3.10+ 推奨(型ヒント構文のため)
# 実行: python encapsulation_example.py
ローカル環境(JavaScript):
# Node.js 18+ 推奨(# プライベートフィールド対応)
# 実行: node encapsulation_example.mjs
Docker環境:
# docker-compose.yml
version: "3.8"
services:
python-demo:
image: python:3.12-slim
volumes:
- ./src:/app
working_dir: /app
command: python encapsulation_example.py
js-demo:
image: node:20-slim
volumes:
- ./src:/app
working_dir: /app
command: node encapsulation_example.mjs
まとめ
カプセル化の本質は「データを隠すこと」ではない。
「責任の境界線を引くこと」 だ。
銀行は金庫にお金を隠しているのではない。「お金を適切に管理する責任を負い、決められたルールでのみ操作を許可する」 から信頼される。カプセル化も同じだ。
- レイヤー1: データを保護する ── 金庫に鍵をかける
- レイヤー2: 操作を窓口化する ── ATMを通じてのみ操作させる
- レイヤー3: 内部実装を隠蔽する ── ATMの裏側は知らなくていい
そして、getter/setter を全属性に機械的に生やすのは「金庫の鍵を全員に配る」行為であり、カプセル化の真逆だ。
次に class を書くとき、こう自問してみてほしい。
「この属性に setter は本当に必要か? それとも、業務操作メソッドで代替できないか?」
その一問が、コードの寿命を劇的に延ばす。
参考文献
- Python公式ドキュメント - クラス
- MDN Web Docs - Private class features
- Martin Fowler「Refactoring」 ── Tell, Don't Ask、Feature Envy の原典
- Robert C. Martin「Clean Code」 ── SOLID原則との関連
この記事が役に立ったと感じたら、いいねとストックをいただけると励みになります。