1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

カプセル化 ≠ getter/setter。その誤解がコードを静かに腐らせる

1
Last updated at Posted at 2026-04-13

この記事の対象読者

  • 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 は本当に必要か? それとも、業務操作メソッドで代替できないか?」

その一問が、コードの寿命を劇的に延ばす。

参考文献


この記事が役に立ったと感じたら、いいねとストックをいただけると励みになります。

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?