MENU

【初心者向け】プログラミングのカプセル化とは?データを守る仕組みをわかりやすく解説

目次

カプセル化って何?基本から理解しよう

カプセル化(Encapsulation)は、オブジェクト指向プログラミング(OOP)の三大要素の一つです。

一言で言うと 「データとそのデータを操作する処理をひとまとめにして、外部から勝手に触れないように保護する仕組み」です。

カプセルという言葉から想像できるように、薬のカプセルのように「中身を包んで保護する」というイメージです。

日常生活で理解するカプセル化

プログラミングの概念は難しそうに聞こえますが、実は私たちの身の回りにもカプセル化の考え方がたくさんあります。

例1:自動販売機で理解する

カプセル化されている自動販売機

  • お金を入れる → ボタンを押す → 商品が出てくる
  • 内部の仕組み(冷却装置、商品の落下機構)は見えない
  • 決められた操作(お金を入れる、ボタンを押す)だけができる
  • 勝手に中の商品を取り出すことはできない

もしカプセル化されていなかったら?

  • 誰でも自由に商品を取り出せる
  • 冷却装置を壊してしまうかもしれない
  • お金を入れずに商品を取れてしまう
  • 管理が不可能になる

自動販売機は「使うための窓口」だけを提供し、内部の複雑な仕組みは隠しています。これがカプセル化の基本的な考え方です。

例2:ATMで理解する

ATMのカプセル化

  • できること:残高照会、預金、引き出し(決められた操作のみ)
  • できないこと:残高を直接書き換える、他人の口座を見る
  • 安全な方法でのみお金を扱える

もしカプセル化がなかったら?

  • 残高を直接10億円に書き換えられる
  • 他人の口座のお金を勝手に動かせる
  • セキュリティが崩壊する

例3:テレビのリモコン

リモコンのカプセル化

  • 使える機能:電源、チャンネル変更、音量調整(ボタンで操作)
  • 隠されている部分:内部の電子回路、信号処理、赤外線通信
  • リモコンを使う人は、内部の仕組みを知らなくても使える

これらの例から分かるように、カプセル化は「使いやすく」「安全に」するための仕組みなのです。

カプセル化がないとどうなる?問題点を知ろう

プログラミングで、カプセル化をしないとどんな問題が起きるのか見てみましょう。

悪い例:銀行口座(カプセル化なし)

// カプセル化されていない銀行口座
class BankAccount {
    constructor() {
        this.balance = 0;  // 残高が誰でも変更できる
    }
}

// 使い方
const account = new BankAccount();
account.balance = 1000;     // 入金
console.log(account.balance); // 1000

// 問題が発生!
account.balance = 1000000;  // 直接100万円に書き換えられる!
account.balance = -5000;    // マイナスの残高にもできてしまう!
account.balance = "abc";    // 文字列も入れられてしまう!

この方法の問題点

  1. 不正な値を設定できる:残高がマイナスになってしまう
  2. データの整合性が保てない:いつ、どこで変更されたか分からない
  3. バグの温床:予期しない動作の原因になる
  4. セキュリティリスク:重要なデータが保護されていない

良い例:銀行口座(カプセル化あり)

// カプセル化された銀行口座
class BankAccount {
    #balance = 0;  // private: 外部から直接アクセスできない
    
    // 入金(検証付き)
    deposit(amount) {
        if (amount > 0) {
            this.#balance += amount;
            console.log(`${amount}円を入金しました`);
            return true;
        }
        console.log('入金額は正の数である必要があります');
        return false;
    }
    
    // 出金(検証付き)
    withdraw(amount) {
        if (amount > 0 && amount <= this.#balance) {
            this.#balance -= amount;
            console.log(`${amount}円を出金しました`);
            return true;
        }
        console.log('出金できません(残高不足または不正な金額)');
        return false;
    }
    
    // 残高照会
    getBalance() {
        return this.#balance;
    }
}

// 使い方
const account = new BankAccount();
account.deposit(1000);       // OK: 1000円を入金しました
account.withdraw(300);       // OK: 300円を出金しました
console.log(account.getBalance());  // 700

// これらの不正な操作はできない
account.deposit(-100);       // NG: 入金額は正の数である必要があります
account.withdraw(10000);     // NG: 出金できません(残高不足)
// account.#balance = 1000000;  // エラー!直接アクセス不可

カプセル化することで、正しい方法でのみデータを扱えるようになります。

カプセル化の3つの重要な要素

カプセル化を実現するための3つの重要な要素を理解しましょう。

要素1:データの隠蔽(情報隠蔽)

内部のデータを外部から直接見たり変更したりできないようにします。

class Person {
    #age;  // private: 外部から見えない
    #salary;  // private: 給与情報も隠す
    
    constructor(age, salary) {
        this.#age = age;
        this.#salary = salary;
    }
    
    // 年齢は公開してもOK
    getAge() {
        return this.#age;
    }
    
    // 給与は本人しか見られない
    #getSalary() {
        return this.#salary;
    }
}

なぜ隠すのか?

  • 不正な変更を防ぐため
  • セキュリティを確保するため
  • データの整合性を保つため

要素2:アクセス制御

データにアクセスする方法を制限します。

class User {
    #email;
    #password;
    
    // メールアドレスの設定(検証付き)
    setEmail(email) {
        // @ マークが含まれているかチェック
        if (email.includes('@')) {
            this.#email = email;
            return true;
        }
        console.log('無効なメールアドレスです');
        return false;
    }
    
    // メールアドレスの取得
    getEmail() {
        return this.#email;
    }
    
    // パスワードは設定のみ可能(取得は不可)
    setPassword(password) {
        if (password.length >= 8) {
            this.#password = this.#hashPassword(password);
            return true;
        }
        console.log('パスワードは8文字以上である必要があります');
        return false;
    }
    
    // パスワードのハッシュ化(内部処理)
    #hashPassword(password) {
        return 'hashed_' + password;
    }
    
    // パスワードの検証
    verifyPassword(inputPassword) {
        return this.#hashPassword(inputPassword) === this.#password;
    }
}

アクセス制御の利点

  • 不正な値の設定を防げる
  • データの検証を一箇所で行える
  • 変更履歴を追跡しやすい

要素3:インターフェースの提供

外部とやり取りするための「窓口」を提供します。

class CoffeeMachine {
    #waterLevel = 100;      // 内部状態
    #coffeeBeansLevel = 50;  // 内部状態
    #isHeating = false;      // 内部状態
    
    // 公開インターフェース:コーヒーを作る
    makeCoffee() {
        if (this.#canMakeCoffee()) {
            this.#heat();
            this.#brew();
            return 'コーヒーができました☕';
        }
        return '材料が不足しています';
    }
    
    // 公開インターフェース:状態確認
    getStatus() {
        return {
            water: this.#waterLevel,
            beans: this.#coffeeBeansLevel
        };
    }
    
    // 内部処理(外部から使えない)
    #canMakeCoffee() {
        return this.#waterLevel >= 10 && this.#coffeeBeansLevel >= 5;
    }
    
    #heat() {
        this.#isHeating = true;
        // 加熱処理
    }
    
    #brew() {
        this.#waterLevel -= 10;
        this.#coffeeBeansLevel -= 5;
        // 抽出処理
    }
}

// 使う側はシンプル
const machine = new CoffeeMachine();
console.log(machine.makeCoffee());  // コーヒーができました☕

インターフェースの役割

  • 複雑な内部処理を隠す
  • 使いやすい操作方法を提供する
  • 利用者は内部の仕組みを知らなくてもOK

アクセス修飾子:カプセル化の実現方法

カプセル化を実現するために、アクセス修飾子を使います。

public(公開):どこからでもアクセス可能

class Example {
    publicProperty = 'みんな見られる';
    
    publicMethod() {
        return 'みんな使える';
    }
}

const obj = new Example();
console.log(obj.publicProperty);  // OK
console.log(obj.publicMethod());  // OK

使う場面

  • 外部に公開したいメソッド
  • 外部から使ってほしい機能

private(非公開):クラス内部のみアクセス可能

class Example {
    #privateProperty = '内部だけ';
    
    #privateMethod() {
        return '内部でだけ使う';
    }
    
    publicMethod() {
        // クラス内部からはアクセスできる
        return this.#privateMethod();
    }
}

const obj = new Example();
// console.log(obj.#privateProperty);  // エラー!
// obj.#privateMethod();  // エラー!
console.log(obj.publicMethod());  // OK

使う場面

  • 外部に見せたくないデータ
  • 内部でのみ使う補助的な処理

protected(保護):継承した子クラスからもアクセス可能

// Javaの例
public class Parent {
    protected String protectedField = "子クラスも使える";
    
    protected void protectedMethod() {
        // 子クラスからも呼び出せる
    }
}

public class Child extends Parent {
    public void useProtected() {
        System.out.println(protectedField);  // OK
        protectedMethod();  // OK
    }
}

使う場面

  • 継承した子クラスに使わせたい機能
  • 完全には隠したくないが、外部には公開したくない場合

Getter と Setter:カプセル化の定番パターン

カプセル化で最もよく使われるのが、GetterとSetterパターンです。

Getterとは?

データを「取得する」ためのメソッドです。

class Product {
    #name;
    #price;
    
    constructor(name, price) {
        this.#name = name;
        this.#price = price;
    }
    
    // Getter: 商品名を取得
    getName() {
        return this.#name;
    }
    
    // Getter: 価格を取得(税込価格で返す)
    getPrice() {
        return Math.floor(this.#price * 1.1);  // 10%の税を追加
    }
}

const product = new Product('ノートPC', 100000);
console.log(product.getName());   // ノートPC
console.log(product.getPrice());  // 110000(税込)

Getterの利点

  • 計算を加えた値を返せる
  • データ形式を変換できる
  • アクセスログを記録できる

Setterとは?

データを「設定する」ためのメソッドです。

class Product {
    #name;
    #price;
    
    // Setter: 商品名を設定(検証付き)
    setName(name) {
        if (name && name.length > 0) {
            this.#name = name;
            return true;
        }
        console.log('商品名は必須です');
        return false;
    }
    
    // Setter: 価格を設定(検証付き)
    setPrice(price) {
        if (typeof price === 'number' && price >= 0) {
            this.#price = price;
            return true;
        }
        console.log('価格は0以上の数値である必要があります');
        return false;
    }
}

const product = new Product();
product.setName('ノートPC');  // OK
product.setPrice(100000);     // OK
product.setPrice(-100);       // NG: 価格は0以上の数値である必要があります
product.setPrice('高い');     // NG: 価格は0以上の数値である必要があります

Setterの利点

  • 値の検証ができる
  • 不正な値を防げる
  • 値を設定する前に処理を挟める

読み取り専用プロパティ

Getterだけを提供し、Setterを提供しない場合もあります。

class User {
    #createdAt;
    
    constructor() {
        this.#createdAt = new Date();  // 作成日時を記録
    }
    
    // Getterのみ:読み取り専用
    getCreatedAt() {
        return this.#createdAt;
    }
    
    // Setterは提供しない:作成日時は変更できない
}

カプセル化のメリット:なぜ重要なのか

メリット1:データの整合性が保たれる

不正な値が設定されるのを防ぎ、常に正しい状態を保てます。

class Rectangle {
    #width;
    #height;
    
    setWidth(width) {
        if (width > 0) {
            this.#width = width;
        } else {
            throw new Error('幅は正の数である必要があります');
        }
    }
    
    setHeight(height) {
        if (height > 0) {
            this.#height = height;
        } else {
            throw new Error('高さは正の数である必要があります');
        }
    }
    
    getArea() {
        return this.#width * this.#height;
    }
}

// 常に正しい値のみが設定される
const rect = new Rectangle();
rect.setWidth(10);   // OK
// rect.setWidth(-5);  // エラー!負の値は設定できない

メリット2:変更の影響を最小限にできる

内部実装を変更しても、外部のコードに影響を与えません。

class UserManager {
    #users;
    
    constructor() {
        // 最初は配列で実装
        this.#users = [];
    }
    
    addUser(user) {
        this.#users.push(user);
    }
    
    getUser(id) {
        return this.#users.find(u => u.id === id);
    }
}

// 後で内部をMapに変更しても...
class UserManager {
    #users;
    
    constructor() {
        // Mapに変更
        this.#users = new Map();
    }
    
    addUser(user) {
        this.#users.set(user.id, user);  // 内部は変更
    }
    
    getUser(id) {
        return this.#users.get(id);  // 内部は変更
    }
}

// 使う側のコードは変更不要!
const manager = new UserManager();
manager.addUser({ id: 1, name: 'Alice' });
const user = manager.getUser(1);

メリット3:使いやすさが向上する

複雑な処理を隠して、シンプルなインターフェースを提供できます。

class ImageProcessor {
    #image;
    
    // 内部の複雑な処理
    #resize(width, height) { /* 複雑な処理 */ }
    #adjustBrightness(level) { /* 複雑な処理 */ }
    #applyFilter(filter) { /* 複雑な処理 */ }
    #compress(quality) { /* 複雑な処理 */ }
    
    // シンプルな公開メソッド
    optimizeForWeb() {
        this.#resize(800, 600);
        this.#adjustBrightness(1.1);
        this.#applyFilter('sharpen');
        this.#compress(85);
        return 'Web用に最適化しました';
    }
}

// 使う側は簡単
const processor = new ImageProcessor();
processor.optimizeForWeb();  // 一発でOK

メリット4:セキュリティが向上する

重要なデータを保護し、不正なアクセスを防げます。

class PasswordManager {
    #hashedPassword;
    #salt;
    
    setPassword(plainPassword) {
        if (plainPassword.length < 8) {
            throw new Error('パスワードは8文字以上必要です');
        }
        this.#salt = this.#generateSalt();
        this.#hashedPassword = this.#hash(plainPassword, this.#salt);
    }
    
    #generateSalt() {
        return Math.random().toString(36);
    }
    
    #hash(password, salt) {
        // ハッシュ化の処理
        return 'hashed_' + password + salt;
    }
    
    verifyPassword(inputPassword) {
        return this.#hash(inputPassword, this.#salt) === this.#hashedPassword;
    }
    
    // 生のパスワードやハッシュ値は絶対に外部に渡さない
}

メリット5:デバッグが簡単になる

データの変更箇所が限定されるため、バグの原因を特定しやすくなります。

class Counter {
    #count = 0;
    
    increment() {
        console.log('increment呼び出し:', new Date());  // ログ
        this.#count++;
    }
    
    decrement() {
        console.log('decrement呼び出し:', new Date());  // ログ
        this.#count--;
    }
    
    getCount() {
        return this.#count;
    }
}

// #countは直接変更できないので、
// ログを見れば、いつ、どこで変更されたか一目瞭然

実践的なカプセル化の例

例1:ショッピングカート

class ShoppingCart {
    #items = [];
    #discountRate = 0;
    
    // 商品を追加
    addItem(product, quantity) {
        if (!product || quantity <= 0) {
            console.log('無効な商品または数量です');
            return false;
        }
        
        this.#items.push({ product, quantity });
        console.log(`${product.name} x ${quantity}を追加しました`);
        return true;
    }
    
    // 商品を削除
    removeItem(productId) {
        const initialLength = this.#items.length;
        this.#items = this.#items.filter(
            item => item.product.id !== productId
        );
        
        if (this.#items.length < initialLength) {
            console.log('商品を削除しました');
            return true;
        }
        return false;
    }
    
    // 割引を適用
    applyDiscount(percentage) {
        if (percentage >= 0 && percentage <= 100) {
            this.#discountRate = percentage;
            console.log(`${percentage}%の割引を適用しました`);
            return true;
        }
        console.log('割引率は0〜100の範囲で指定してください');
        return false;
    }
    
    // 小計を計算(内部処理)
    #calculateSubtotal() {
        return this.#items.reduce((sum, item) => {
            return sum + (item.product.price * item.quantity);
        }, 0);
    }
    
    // 割引額を計算(内部処理)
    #calculateDiscount() {
        return this.#calculateSubtotal() * (this.#discountRate / 100);
    }
    
    // 合計金額を取得
    getTotal() {
        return this.#calculateSubtotal() - this.#calculateDiscount();
    }
    
    // カート内の商品数を取得
    getItemCount() {
        return this.#items.reduce((count, item) => {
            return count + item.quantity;
        }, 0);
    }
    
    // カートの内容を取得(読み取り専用のコピー)
    getItems() {
        return [...this.#items];  // コピーを返す
    }
}

// 使い方
const cart = new ShoppingCart();
cart.addItem({ id: 1, name: 'ノートPC', price: 100000 }, 1);
cart.addItem({ id: 2, name: 'マウス', price: 2000 }, 2);
cart.applyDiscount(10);  // 10%割引
console.log(`合計: ${cart.getTotal()}円`);  // 合計: 93600円
console.log(`商品数: ${cart.getItemCount()}個`);  // 商品数: 3個

例2:タイマークラス

class Timer {
    #seconds = 0;
    #isRunning = false;
    #intervalId = null;
    #callbacks = [];
    
    // タイマー開始
    start() {
        if (this.#isRunning) {
            console.log('タイマーは既に動作中です');
            return false;
        }
        
        this.#isRunning = true;
        this.#intervalId = setInterval(() => {
            this.#tick();
        }, 1000);
        
        console.log('タイマーを開始しました');
        return true;
    }
    
    // タイマー停止
    stop() {
        if (!this.#isRunning) {
            console.log('タイマーは停止しています');
            return false;
        }
        
        this.#isRunning = false;
        clearInterval(this.#intervalId);
        console.log('タイマーを停止しました');
        return true;
    }
    
    // タイマーリセット
    reset() {
        this.stop();
        this.#seconds = 0;
        console.log('タイマーをリセットしました');
    }
    
    // 1秒ごとの処理(内部)
    #tick() {
        this.#seconds++;
        this.#notifyCallbacks();
    }
    
    // コールバック通知(内部)
    #notifyCallbacks() {
        this.#callbacks.forEach(callback => {
            callback(this.getTime());
        });
    }
    
    // 時間を取得
    getTime() {
        const hours = Math.floor(this.#seconds / 3600);
        const minutes = Math.floor((this.#seconds % 3600) / 60);
        const seconds = this.#seconds % 60;
        
        return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
    }
    
    // 秒数を取得
    getSeconds() {
        return this.#seconds;
    }
    
    // 動作状態を確認
    isRunning() {
        return this.#isRunning;
    }
    
    // コールバックを登録
    onChange(callback) {
        this.#callbacks.push(callback);
    }
}

// 使い方
const timer = new Timer();
timer.onChange((time) => {
    console.log(`経過時間: ${time}`);
});

timer.start();
// 3秒後
setTimeout(() => {
    console.log(timer.getTime());  // 00:00:03
    timer.stop();
}, 3000);

主要プログラミング言語でのカプセル化

JavaScript(ES2022+)

class Example {
    #privateField = 'private';  // # で private
    publicField = 'public';
    
    #privateMethod() {
        return 'private method';
    }
    
    publicMethod() {
        return this.#privateMethod();
    }
}

TypeScript

class Example {
    private privateField: string;
    protected protectedField: string;
    public publicField: string;
    
    constructor() {
        this.privateField = 'private';
        this.protectedField = 'protected';
        this.publicField = 'public';
    }
}

Java

public class Example {
    private String privateField;
    protected String protectedField;
    public String publicField;
    
    // Getter
    public String getPrivateField() {
        return privateField;
    }
    
    // Setter
    public void setPrivateField(String value) {
        this.privateField = value;
    }
}

Python

class Example:
    def __init__(self):
        self.__private = 'private'  # __ で private(慣習)
        self._protected = 'protected'  # _ で protected(慣習)
        self.public = 'public'
    
    def get_private(self):
        return self.__private

C#

public class Example {
    private string privateField;
    protected string protectedField;
    public string publicField;
    
    // プロパティ(自動Getter/Setter)
    public string Name { get; set; }
    
    // 読み取り専用プロパティ
    public string ReadOnly { get; private set; }
}

カプセル化の注意点:やりすぎに注意

注意点1:過度なカプセル化は避ける

全てをprivateにする必要はありません。

// 過度なカプセル化(悪い例)
class Point {
    #x;
    #y;
    
    getX() { return this.#x; }
    setX(x) { this.#x = x; }
    getY() { return this.#y; }
    setY(y) { this.#y = y; }
}

// 適切なバランス(良い例)
class Point {
    x;  // 単純な座標ならpublicでもOK
    y;
    
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    
    // 意味のある操作だけメソッド化
    distanceTo(other) {
        const dx = this.x - other.x;
        const dy = this.y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

注意点2:無意味なGetter/Setterは作らない

単にフィールドを隠すためだけのGetter/Setterは意味がありません。

// 意味がない例
class User {
    #name;
    
    getName() { return this.#name; }  // ただ返すだけ
    setName(name) { this.#name = name; }  // ただ設定するだけ
}

// 検証や処理があるなら意味がある
class User {
    #name;
    
    getName() {
        return this.#name;
    }
    
    setName(name) {
        // 検証がある
        if (name && name.length > 0) {
            this.#name = name;
        } else {
            throw new Error('名前は必須です');
        }
    }
}

判断基準

  • 検証や変換が必要 → Getter/Setterを使う
  • 単なるデータの保持 → publicでもOK

注意点3:パフォーマンスへの影響

過度なメソッド呼び出しは、パフォーマンスに影響する場合があります。

// 頻繁にアクセスされる単純なプロパティ
class Vector {
    // これはpublicでもOK
    x = 0;
    y = 0;
    z = 0;
    
    // 計算が必要な場合はメソッド化
    getLength() {
        return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
    }
}

よくある質問(FAQ)

Q1:全てのプロパティをprivateにすべきですか?

A:いいえ、必要に応じて判断しましょう

  • privateにすべき:重要なデータ、検証が必要なデータ、変更を追跡したいデータ
  • publicでもOK:単純な値、頻繁にアクセスされる値、変更しても問題ない値

例えば、座標の x, y や、色の RGB値などは、publicでも問題ない場合が多いです。

Q2:Getter/Setterは必ず必要ですか?

A:検証や処理が必要な場合に使いましょう

// Getter/Setterが必要な例
class BankAccount {
    #balance = 0;
    
    deposit(amount) {
        if (amount > 0) {  // 検証が必要
            this.#balance += amount;
        }
    }
}

// 不要な例(シンプルなデータ)
class Point {
    x = 0;
    y = 0;
}

Q3:JavaScriptの # とTypeScriptの private の違いは?

A:実行時の動作が異なります

  • JavaScript の #:実行時にも本当にアクセスできない
  • TypeScript の private:コンパイル時のチェックのみ、実行時はアクセス可能
// TypeScript
class Example {
    private value = 10;
}

// コンパイル後のJavaScript
class Example {
    value = 10;  // privateが消える
}

Q4:継承とカプセル化はどう関係しますか?

A:protectedを使って、子クラスに必要な機能を公開します

class Animal {
    #name;  // 完全にprivate
    
    constructor(name) {
        this.#name = name;
    }
    
    // protectedのような動作(JavaScriptには直接ない)
    _getInternalName() {
        return this.#name;
    }
}

class Dog extends Animal {
    bark() {
        // 親の_getInternalNameを使える
        console.log(`${this._getInternalName()}がワンワン`);
    }
}

Q5:カプセル化するとコード量が増えませんか?

A:増えますが、長期的にはメリットの方が大きいです

短期的なコスト

  • コード量が増える
  • 書くのに時間がかかる

長期的なメリット

  • バグが減る
  • 保守が簡単になる
  • 変更に強くなる
  • チーム開発がしやすくなる

大規模プロジェクトや長期運用では、初期のコスト増加を上回るメリットがあります。

カプセル化と他のOOP概念の関係

カプセル化は、他のオブジェクト指向の概念と組み合わせて使います。

カプセル化 × 継承

class Vehicle {
    #fuel = 0;
    
    refuel(amount) {
        if (amount > 0) {
            this.#fuel += amount;
        }
    }
    
    _getFuel() {  // 子クラス用
        return this.#fuel;
    }
}

class Car extends Vehicle {
    drive() {
        if (this._getFuel() > 0) {
            console.log('運転中...');
            // fuelを減らす処理
        } else {
            console.log('ガソリンがありません');
        }
    }
}

カプセル化 × ポリモーフィズム

class Shape {
    #color;
    
    constructor(color) {
        this.#color = color;
    }
    
    getColor() {
        return this.#color;
    }
    
    // 各図形で実装が異なる
    getArea() {
        throw new Error('サブクラスで実装してください');
    }
}

class Circle extends Shape {
    #radius;
    
    constructor(color, radius) {
        super(color);
        this.#radius = radius;
    }
    
    getArea() {
        return Math.PI * this.#radius * this.#radius;
    }
}

class Rectangle extends Shape {
    #width;
    #height;
    
    constructor(color, width, height) {
        super(color);
        this.#width = width;
        this.#height = height;
    }
    
    getArea() {
        return this.#width * this.#height;
    }
}

// 使い方:内部実装は隠されている
const shapes = [
    new Circle('赤', 5),
    new Rectangle('青', 10, 20)
];

shapes.forEach(shape => {
    console.log(`色: ${shape.getColor()}, 面積: ${shape.getArea()}`);
});

実務でのカプセル化のベストプラクティス

プラクティス1:意味のある名前を付ける

// 悪い例
class C {
    #d;
    getD() { return this.#d; }
}

// 良い例
class User {
    #createdDate;
    getCreatedDate() { return this.#createdDate; }
}

プラクティス2:単一責任の原則を守る

一つのクラスは一つの責任だけを持つべきです。

// 悪い例:複数の責任を持つ
class User {
    #name;
    #email;
    
    // ユーザー管理
    getName() { return this.#name; }
    
    // メール送信(別の責任)
    sendEmail(message) {
        // メール送信処理
    }
    
    // データベース操作(別の責任)
    save() {
        // DB保存処理
    }
}

// 良い例:責任を分離
class User {
    #name;
    #email;
    
    getName() { return this.#name; }
    getEmail() { return this.#email; }
}

class EmailService {
    sendEmail(user, message) {
        // メール送信処理
    }
}

class UserRepository {
    save(user) {
        // DB保存処理
    }
}

プラクティス3:不変オブジェクトを活用する

一度作成したら変更できないオブジェクトは、バグが少なくなります。

class ImmutablePoint {
    #x;
    #y;
    
    constructor(x, y) {
        this.#x = x;
        this.#y = y;
    }
    
    // Getterのみ(Setterなし)
    getX() { return this.#x; }
    getY() { return this.#y; }
    
    // 新しいインスタンスを返す
    move(dx, dy) {
        return new ImmutablePoint(this.#x + dx, this.#y + dy);
    }
}

const p1 = new ImmutablePoint(0, 0);
const p2 = p1.move(10, 20);  // 新しいインスタンス
console.log(p1.getX(), p1.getY());  // 0, 0(変わらない)
console.log(p2.getX(), p2.getY());  // 10, 20

プラクティス4:防御的コピーを使う

配列やオブジェクトを返すときは、コピーを返しましょう。

class TaskList {
    #tasks = [];
    
    addTask(task) {
        this.#tasks.push(task);
    }
    
    // 悪い例:内部配列の参照を返す
    getTasks_bad() {
        return this.#tasks;  // 外部から変更可能!
    }
    
    // 良い例:コピーを返す
    getTasks() {
        return [...this.#tasks];  // コピーを返す
    }
}

const list = new TaskList();
list.addTask('タスク1');

const tasks = list.getTasks();
tasks.push('不正なタスク');  // コピーなので内部は変わらない

まとめ:カプセル化でより良いコードを書こう

カプセル化は、データと処理をまとめ、外部から内部の詳細を隠す重要な仕組みです。

重要ポイントまとめ

  • データを直接アクセスできないように保護する
  • アクセス修飾子(public、private、protected)を使い分ける
  • Getter/Setterで検証や変換を行う
  • データの整合性、セキュリティ、保守性が向上する
  • 過度なカプセル化は避け、適切なバランスを保つ
  • 継承やポリモーフィズムと組み合わせて使う

初心者へのアドバイス

  1. まずは重要なデータ(残高、パスワードなど)をprivateにする
  2. 検証が必要な場合はSetter を使う
  3. 徐々にカプセル化の範囲を広げる
  4. 「本当に隠す必要があるか?」を常に考える
  5. 実際にコードを書いて練習する

学習の次のステップ

  • 継承と組み合わせた使い方を学ぶ
  • デザインパターン(Factory、Singletonなど)を学ぶ
  • SOLID原則を理解する
  • 実際のプロジェクトで実践する

カプセル化を適切に使うことで、安全で保守しやすく、変更に強いコードが書けるようになります。最初は少し手間に感じるかもしれませんが、プロジェクトが大きくなるほど、その価値が実感できるはずです。

オブジェクト指向プログラミングの基本として、ぜひカプセル化をマスターしましょう!

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次