「クラス構文で整理する – オブジェクト指向プログラミングの現代的アプローチ」

「プロトタイプベースのJavaScriptでクラスっぽく書くのが難しい…」

「privateな変数やメソッドってどうやって実現するの?」

「継承を使いこなせる気がしない…」

前回はテンプレートリテラルについて学びました。今回は、ES6で導入されたクラス構文を使って、より構造化されたコードの書き方を学んでいきましょう。

従来のオブジェクト指向スタイル

まずは、従来のJavaScriptでのクラスライクな実装を見てみましょう:

JS
// コンストラクタ関数とプロトタイプを使用
function User(name, email) {
    this.name = name;
    this.email = email;
}

User.prototype.sayHello = function() {
    return 'Hello, ' + this.name + '!';
};

// 継承の実装
function Admin(name, email, role) {
    User.call(this, name, email);
    this.role = role;
}

Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;

この方法の問題点:

  • 構文が直感的でない
  • 継承の実装が複雑
  • privateなプロパティの実現が困難
  • コードが冗長になりやすい

モダンなクラス構文

JS
class User {
    // プライベートフィールド
    #lastLoginTime;
    
    constructor(name, email) {
        this.name = name;
        this.email = email;
        this.#lastLoginTime = new Date();
    }
    
    // メソッド
    sayHello() {
        return `Hello, ${this.name}!`;
    }
    
    // ゲッター
    get lastLogin() {
        return this.#lastLoginTime;
    }
    
    // 静的メソッド
    static createGuest() {
        return new User('Guest', 'guest@example.com');
    }
}

クラス構文の特徴:

  • 直感的な構文
  • privateフィールドのサポート
  • ゲッター/セッターの簡単な実装
  • 静的メソッドの明確な定義

継承の実装

JS
class Admin extends User {
    constructor(name, email, role) {
        // 親クラスのコンストラクタを呼び出し
        super(name, email);
        this.role = role;
    }
    
    // メソッドのオーバーライド
    sayHello() {
        return `${super.sayHello()} (${this.role})`;
    }
    
    // 管理者固有のメソッド
    manageUsers() {
        return `${this.name} is managing users`;
    }
}

継承のポイント:

  • extendsキーワードで簡単に継承
  • superで親クラスのメソッドにアクセス
  • メソッドの上書きが容易

プライバシーとカプセル化

JS
class BankAccount {
    #balance = 0;
    #transactions = [];
    
    constructor(initialBalance) {
        this.#balance = initialBalance;
    }
    
    deposit(amount) {
        if (amount <= 0) throw new Error('Invalid amount');
        this.#balance += amount;
        this.#addTransaction('deposit', amount);
    }
    
    withdraw(amount) {
        if (amount <= 0) throw new Error('Invalid amount');
        if (amount > this.#balance) throw new Error('Insufficient funds');
        
        this.#balance -= amount;
        this.#addTransaction('withdraw', amount);
    }
    
    #addTransaction(type, amount) {
        this.#transactions.push({
            type,
            amount,
            date: new Date()
        });
    }
    
    get balance() {
        return this.#balance;
    }
    
    get transactionHistory() {
        return [...this.#transactions];
    }
}

カプセル化のポイント:

  • プライベートフィールドは#で定義
  • 内部データの直接操作を防止
  • ゲッターで安全なデータアクセスを提供

実践的なデザインパターン

シングルトン

JS
class Database {
    static #instance;
    #connections = [];
    
    constructor() {
        if (Database.#instance) {
            return Database.#instance;
        }
        Database.#instance = this;
    }
    
    connect(config) {
        this.#connections.push(config);
        console.log('Connected to database');
    }
    
    static getInstance() {
        if (!Database.#instance) {
            Database.#instance = new Database();
        }
        return Database.#instance;
    }
}

ファクトリー

JS
class UserFactory {
    static createUser(type, data) {
        switch (type) {
            case 'regular':
                return new User(data.name, data.email);
            case 'admin':
                return new Admin(data.name, data.email, data.role);
            default:
                throw new Error('Unknown user type');
        }
    }
}

コンポーネント設計の例

JS
class Component {
    #element;
    
    constructor(selector) {
        this.#element = document.querySelector(selector);
    }
    
    render() {
        throw new Error('render method must be implemented');
    }
    
    update() {
        if (this.#element) {
            this.#element.innerHTML = this.render();
        }
    }
}

class UserList extends Component {
    #users = [];
    
    constructor(selector) {
        super(selector);
    }
    
    setUsers(users) {
        this.#users = users;
        this.update();
    }
    
    render() {
        return `
            <ul class="user-list">
                ${this.#users.map(user => `
                    <li class="user-item">
                        <strong>${user.name}</strong>
                        <span>${user.email}</span>
                    </li>
                `).join('')}
            </ul>
        `;
    }
}

エラーハンドリング

JS
class ValidationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ValidationError';
    }
}

class UserService {
    #validateUser(user) {
        if (!user.name) {
            throw new ValidationError('Name is required');
        }
        if (!user.email.includes('@')) {
            throw new ValidationError('Invalid email format');
        }
    }
    
    createUser(userData) {
        try {
            this.#validateUser(userData);
            return new User(userData.name, userData.email);
        } catch (error) {
            if (error instanceof ValidationError) {
                console.error('Validation failed:', error.message);
            } else {
                console.error('Unexpected error:', error);
            }
            throw error;
        }
    }
}

まとめ

クラス構文を使うことで:

  • コードの構造が明確に
  • カプセル化が容易に
  • 継承が直感的に
  • メンテナンス性が向上

次回は「実践編:jQueryプラグインをVanilla JSで書き直す」について解説し、これまで学んだ知識を活用した実践的なリファクタリングを行っていきましょう。