JavaScriptのクラスフィールドのアクセス指定子の使い方を徹底解説

JavaScriptのクラスフィールドとアクセス指定子の基本概念と重要性を紹介します。

JavaScriptは、従来のプロトタイプベースのオブジェクト指向言語として知られていましたが、ES6以降、クラス構文が導入され、オブジェクト指向プログラミングがより直感的に行えるようになりました。このクラス構文の中で重要な概念の一つが「クラスフィールド」と「アクセス指定子」です。

クラスフィールドとは、クラスのインスタンスごとに異なるデータを保持するための変数を指します。これにより、オブジェクトの状態を管理することが容易になります。また、アクセス指定子は、クラス内のフィールドやメソッドに対するアクセスレベルを制御するためのキーワードです。これにより、カプセル化を実現し、オブジェクトの内部状態を外部から保護することができます。

本記事では、JavaScriptにおけるクラスフィールドとアクセス指定子の基本的な使い方から、実際の開発での応用例、エラーハンドリング、ベストプラクティスに至るまで、詳しく解説します。これにより、JavaScriptでのクラス設計がより効率的かつ効果的になるでしょう。

目次

クラスフィールドとは何か

クラスフィールドは、クラスのインスタンスごとに異なるデータを保持するための変数です。JavaScriptにおいては、クラスの中で直接宣言され、クラスの各インスタンスが独自の値を持つことができます。

クラスフィールドの定義

クラスフィールドは、クラスの内部で次のように宣言します。

class Person {
  name; // クラスフィールドの宣言
  constructor(name) {
    this.name = name; // クラスフィールドの初期化
  }
}

const john = new Person("John");
console.log(john.name); // "John"

この例では、nameというクラスフィールドを持つPersonクラスを定義しています。クラスフィールドは、constructor内でthisキーワードを使って初期化されます。

クラスフィールドの基本的な使用例

クラスフィールドを使用すると、各インスタンスに固有のデータを保持できます。例えば、次のようにして個々の人物の情報を管理できます。

class Person {
  name;
  age;
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  getDetails() {
    return `${this.name} is ${this.age} years old.`;
  }
}

const alice = new Person("Alice", 30);
const bob = new Person("Bob", 25);

console.log(alice.getDetails()); // "Alice is 30 years old."
console.log(bob.getDetails());   // "Bob is 25 years old."

この例では、Personクラスはnameageという2つのクラスフィールドを持ち、それぞれのインスタンス(alicebob)が異なる値を保持しています。これにより、オブジェクト指向の設計が直感的かつ明確になります。

クラスフィールドは、オブジェクトの状態を管理し、クラスのインスタンス間で一貫したデータ操作を行うための重要な要素です。次に、アクセス指定子の種類とその役割について詳しく見ていきます。

アクセス指定子の種類と役割

アクセス指定子は、クラスのフィールドやメソッドに対するアクセスレベルを制御するためのキーワードです。これにより、オブジェクトの内部状態を保護し、カプセル化を実現します。JavaScriptにおける主なアクセス指定子には、publicprivateprotectedがあります。

public

publicアクセス指定子は、クラスのフィールドやメソッドが外部からアクセス可能であることを示します。JavaScriptでは、デフォルトで全てのクラスメンバーがpublicとみなされます。

class Animal {
  public name;
  constructor(name) {
    this.name = name;
  }

  public speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

const dog = new Animal("Dog");
dog.speak(); // "Dog makes a sound."
console.log(dog.name); // "Dog"

この例では、nameフィールドとspeakメソッドがpublicとして宣言されており、クラス外部からアクセスできます。

private

privateアクセス指定子は、クラスのフィールドやメソッドがクラス内部でのみアクセス可能であることを示します。privateフィールドはクラス外部から直接アクセスすることはできません。

class Car {
  #brand;
  constructor(brand) {
    this.#brand = brand;
  }

  getBrand() {
    return this.#brand;
  }
}

const myCar = new Car("Toyota");
console.log(myCar.getBrand()); // "Toyota"
console.log(myCar.#brand); // エラー: Private field '#brand' must be declared in an enclosing class

この例では、#brandフィールドがprivateとして宣言されており、クラス外部から直接アクセスするとエラーが発生します。

protected

protectedアクセス指定子は、クラスとそのサブクラス内でのみアクセス可能なフィールドやメソッドを示します。JavaScriptでは直接的なprotectedキーワードは存在しませんが、#記号を使って定義されたprivateフィールドをサブクラスでアクセスできるようにするための設計を工夫することができます。

class Employee {
  #id;
  constructor(id) {
    this.#id = id;
  }

  getId() {
    return this.#id;
  }
}

class Manager extends Employee {
  constructor(id) {
    super(id);
  }

  displayId() {
    console.log(`Manager ID: ${this.getId()}`);
  }
}

const manager = new Manager(123);
manager.displayId(); // "Manager ID: 123"

この例では、Employeeクラスの#idフィールドはprivateですが、getIdメソッドを通じてサブクラスManagerからアクセスできます。protectedの概念を実現するためにメソッドを活用しています。

アクセス指定子を適切に使用することで、クラスの設計がより堅牢になり、コードの保守性と安全性が向上します。次に、それぞれのアクセス指定子の具体的な使用方法を詳しく見ていきましょう。

publicフィールドの使い方

publicフィールドは、クラスの外部から直接アクセス可能なフィールドです。JavaScriptでは、クラスフィールドはデフォルトでpublicと見なされます。これにより、外部のコードからフィールドにアクセスしたり、値を変更したりすることができます。

publicフィールドの定義方法

publicフィールドはクラスの内部で直接宣言し、クラスの外部からアクセス可能にします。

class Person {
  name;
  age;

  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  introduce() {
    console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
  }
}

const alice = new Person("Alice", 30);
console.log(alice.name); // "Alice"
console.log(alice.age);  // 30
alice.introduce();       // "Hi, I'm Alice and I'm 30 years old."

この例では、nameageフィールドがpublicとして定義されており、インスタンスaliceを通じてクラス外部からアクセスできます。

publicフィールドの使用例

publicフィールドは、オブジェクトの状態を外部から直接取得したり、設定したりする場面で使用されます。例えば、次のようにしてPersonクラスのインスタンスのプロパティにアクセスし、変更することができます。

class Book {
  title;
  author;

  constructor(title, author) {
    this.title = title;
    this.author = author;
  }

  getDetails() {
    return `${this.title} by ${this.author}`;
  }
}

const book1 = new Book("1984", "George Orwell");
console.log(book1.title);       // "1984"
console.log(book1.author);      // "George Orwell"
console.log(book1.getDetails()); // "1984 by George Orwell"

// publicフィールドの値を変更
book1.title = "Animal Farm";
book1.author = "George Orwell";
console.log(book1.getDetails()); // "Animal Farm by George Orwell"

この例では、Bookクラスのtitleauthorフィールドがpublicとして定義されており、インスタンスbook1を通じてクラス外部からアクセスし、値を変更することができます。

publicフィールドの使用は、クラスのインターフェースを単純化し、外部からのアクセスを容易にします。しかし、必要に応じてアクセス制限をかけることで、オブジェクトの内部状態を保護することも重要です。次に、privateフィールドの使い方について詳しく見ていきましょう。

privateフィールドの使い方

privateフィールドは、クラスの外部からアクセスできないフィールドです。JavaScriptでは、#記号を使ってprivateフィールドを定義します。これにより、クラス内部からのみアクセスでき、外部からの不正な操作を防ぎます。

privateフィールドの定義方法

privateフィールドは、クラス内部で#記号を使って宣言します。クラス外部からは直接アクセスできません。

class Person {
  #name;
  #age;

  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

  introduce() {
    console.log(`Hi, I'm ${this.#name} and I'm ${this.#age} years old.`);
  }

  getName() {
    return this.#name;
  }

  getAge() {
    return this.#age;
  }
}

const alice = new Person("Alice", 30);
console.log(alice.getName()); // "Alice"
console.log(alice.getAge());  // 30
alice.introduce();            // "Hi, I'm Alice and I'm 30 years old."
console.log(alice.#name);     // エラー: Private field '#name' must be declared in an enclosing class

この例では、#name#ageフィールドがprivateとして定義されており、クラス外部から直接アクセスするとエラーが発生します。

privateフィールドの使用例

privateフィールドは、クラスの内部状態を外部から保護するために使用されます。これにより、データの整合性と安全性が確保されます。

class BankAccount {
  #balance;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`Deposited: $${amount}`);
    } else {
      console.log('Deposit amount must be positive');
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`Withdrew: $${amount}`);
    } else {
      console.log('Invalid withdrawal amount');
    }
  }

  getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);           // "Deposited: $500"
account.withdraw(300);          // "Withdrew: $300"
console.log(account.getBalance()); // 1200
console.log(account.#balance);  // エラー: Private field '#balance' must be declared in an enclosing class

この例では、BankAccountクラスの#balanceフィールドがprivateとして定義されており、クラス外部から直接アクセスすることはできません。depositwithdrawメソッドを通じてのみ、#balanceフィールドに対する操作が行われます。

privateフィールドを使用することで、クラスの内部状態を保護し、外部からの不正なアクセスや操作を防ぐことができます。次に、protectedフィールドの使い方について詳しく見ていきましょう。

protectedフィールドの使い方

protectedフィールドは、クラスとそのサブクラス内でのみアクセス可能なフィールドです。JavaScriptには直接のprotectedキーワードはありませんが、設計によって同様の機能を実現できます。これにより、継承関係にあるクラス間でのみデータを共有し、外部からはアクセスできないようにします。

protectedフィールドの定義方法

JavaScriptではprotectedフィールドを直接定義することはできませんが、privateフィールドとメソッドを組み合わせることで、protectedフィールドに相当する動作を実現できます。

class Employee {
  #name;
  #age;

  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

  getDetails() {
    return `${this.#name}, ${this.#age} years old`;
  }

  protectedMethod() {
    return `Protected access to ${this.#name}`;
  }
}

class Manager extends Employee {
  constructor(name, age, department) {
    super(name, age);
    this.department = department;
  }

  getManagerDetails() {
    return `${super.getDetails()}, Department: ${this.department}`;
  }

  accessProtectedMethod() {
    return this.protectedMethod();
  }
}

const manager = new Manager("Alice", 40, "HR");
console.log(manager.getManagerDetails()); // "Alice, 40 years old, Department: HR"
console.log(manager.accessProtectedMethod()); // "Protected access to Alice"

この例では、Employeeクラスの#name#ageフィールドはprivateとして定義されていますが、protectedMethodメソッドを通じてサブクラスManager内でアクセスされています。ManagerクラスはEmployeeクラスを継承し、protectedMethodメソッドを利用してprotectedのような動作を実現しています。

protectedフィールドの使用例

protectedフィールドは、サブクラス内で共有しつつ、クラスの外部からはアクセスを制限する場面で使用されます。

class Vehicle {
  #speed;

  constructor(speed) {
    this.#speed = speed;
  }

  getSpeed() {
    return this.#speed;
  }

  protectedSpeedMethod() {
    return `Protected speed: ${this.#speed}`;
  }
}

class Car extends Vehicle {
  constructor(speed, model) {
    super(speed);
    this.model = model;
  }

  getCarDetails() {
    return `Model: ${this.model}, Speed: ${super.getSpeed()}`;
  }

  accessProtectedSpeed() {
    return this.protectedSpeedMethod();
  }
}

const myCar = new Car(120, "Toyota");
console.log(myCar.getCarDetails()); // "Model: Toyota, Speed: 120"
console.log(myCar.accessProtectedSpeed()); // "Protected speed: 120"

この例では、Vehicleクラスの#speedフィールドはprivateとして定義されていますが、protectedSpeedMethodメソッドを通じてサブクラスCar内でアクセスされています。これにより、protectedのような動作を実現しています。

protectedフィールドの設計は、クラス間のデータ共有と外部からのデータ保護を両立させるために重要です。次に、アクセス指定子の応用例について詳しく見ていきましょう。

アクセス指定子の応用例

アクセス指定子を適切に使用することで、コードの可読性と保守性を高め、データの安全性を確保できます。ここでは、実際の開発でのアクセス指定子の使用例とその利点を紹介します。

銀行システムにおけるアクセス指定子の使用例

銀行システムでは、顧客情報や取引情報を適切に管理し、外部からの不正なアクセスを防ぐことが重要です。以下に、publicprivateprotectedアクセス指定子を使用したクラス設計の例を示します。

class BankAccount {
  #balance;
  #accountNumber;
  protected ownerName;

  constructor(ownerName, accountNumber, initialBalance) {
    this.ownerName = ownerName;
    this.#accountNumber = accountNumber;
    this.#balance = initialBalance;
  }

  public deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`Deposited: $${amount}`);
    } else {
      console.log('Deposit amount must be positive');
    }
  }

  public withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`Withdrew: $${amount}`);
    } else {
      console.log('Invalid withdrawal amount');
    }
  }

  public getBalance() {
    return this.#balance;
  }

  public getAccountNumber() {
    return this.#accountNumber;
  }

  protected getOwnerName() {
    return this.ownerName;
  }
}

class SavingsAccount extends BankAccount {
  constructor(ownerName, accountNumber, initialBalance, interestRate) {
    super(ownerName, accountNumber, initialBalance);
    this.interestRate = interestRate;
  }

  public addInterest() {
    const interest = this.getBalance() * this.interestRate / 100;
    this.deposit(interest);
    console.log(`Interest added: $${interest}`);
  }

  public displayOwner() {
    console.log(`Account Owner: ${this.getOwnerName()}`);
  }
}

const mySavings = new SavingsAccount("John Doe", "123456789", 1000, 2);
mySavings.deposit(500);           // "Deposited: $500"
mySavings.addInterest();          // "Interest added: $30"
console.log(mySavings.getBalance()); // 1530
mySavings.displayOwner();         // "Account Owner: John Doe"
console.log(mySavings.getAccountNumber()); // "123456789"
console.log(mySavings.#balance);  // エラー: Private field '#balance' must be declared in an enclosing class

この例では、BankAccountクラスに#balance#accountNumberというprivateフィールド、ownerNameというprotectedフィールドが定義されています。これにより、クラス外部からの直接アクセスを防ぎつつ、必要に応じてサブクラスSavingsAccount内でアクセスできるようにしています。

アクセス指定子の利点

アクセス指定子を適切に使用することで、以下の利点が得られます。

  1. データのカプセル化:クラスの内部状態を外部から隠蔽することで、データの不整合や不正アクセスを防ぎます。
  2. コードの可読性:アクセス制御を明示することで、クラスの設計意図が明確になり、コードの可読性が向上します。
  3. 保守性の向上:クラス内部の変更が外部に影響を与えにくくなるため、コードの保守が容易になります。
  4. 再利用性の向上:適切にアクセス制御されたクラスは、他のプロジェクトやコンテキストでも再利用しやすくなります。

アクセス指定子を効果的に活用することで、堅牢で保守性の高いコードを実現できます。次に、アクセス指定子とメソッドの関係について詳しく見ていきましょう。

アクセス指定子とメソッドの関係

アクセス指定子は、クラス内のメソッドにも適用できます。これにより、メソッドがどの範囲でアクセス可能かを制御し、クラスの設計をより堅牢にします。ここでは、publicprivateprotectedメソッドの使い方とその関係について説明します。

publicメソッドの使い方

publicメソッドは、クラスの外部から呼び出すことができるメソッドです。一般的に、クラスのインターフェースを提供し、外部とのやり取りを行うために使用されます。

class User {
  #username;
  #password;

  constructor(username, password) {
    this.#username = username;
    this.#password = password;
  }

  public getUsername() {
    return this.#username;
  }

  public setUsername(newUsername) {
    this.#username = newUsername;
  }

  public checkPassword(password) {
    return this.#password === password;
  }
}

const user = new User("john_doe", "securePass123");
console.log(user.getUsername()); // "john_doe"
user.setUsername("jane_doe");
console.log(user.getUsername()); // "jane_doe"
console.log(user.checkPassword("securePass123")); // true

この例では、getUsernamesetUsernamecheckPasswordの各メソッドがpublicとして定義されており、クラス外部から呼び出すことができます。

privateメソッドの使い方

privateメソッドは、クラスの外部から直接呼び出すことができず、クラス内部でのみ使用されます。内部ロジックやヘルパーメソッドを隠蔽するために使用します。

class PasswordManager {
  #password;

  constructor(password) {
    this.#password = password;
  }

  public updatePassword(newPassword) {
    if (this.#validatePassword(newPassword)) {
      this.#password = newPassword;
      console.log("Password updated successfully.");
    } else {
      console.log("Password update failed. Invalid password.");
    }
  }

  #validatePassword(password) {
    return password.length >= 8;
  }
}

const manager = new PasswordManager("initialPass");
manager.updatePassword("newSecurePass123"); // "Password updated successfully."
manager.updatePassword("short"); // "Password update failed. Invalid password."
console.log(manager.#validatePassword("test")); // エラー: Private field '#validatePassword' must be declared in an enclosing class

この例では、#validatePasswordメソッドがprivateとして定義されており、クラス外部から直接呼び出すことはできません。updatePasswordメソッド内でのみ使用されています。

protectedメソッドの使い方

JavaScriptには直接的なprotectedキーワードはありませんが、設計によってprotectedのような機能を持つメソッドを実現できます。サブクラスでのみアクセス可能にするためには、親クラスでprivateメソッドを使用し、それを呼び出すpublicまたはprotectedメソッドを定義します。

class Employee {
  #name;
  #position;

  constructor(name, position) {
    this.#name = name;
    this.#position = position;
  }

  public getDetails() {
    return `${this.#name} is a ${this.#position}`;
  }

  #getPosition() {
    return this.#position;
  }

  protected accessPosition() {
    return this.#getPosition();
  }
}

class Manager extends Employee {
  constructor(name, position, department) {
    super(name, position);
    this.department = department;
  }

  public getManagerDetails() {
    return `${super.getDetails()}, Department: ${this.department}`;
  }

  public displayPosition() {
    console.log(`Position: ${this.accessPosition()}`);
  }
}

const manager = new Manager("Alice", "Manager", "HR");
console.log(manager.getManagerDetails()); // "Alice is a Manager, Department: HR"
manager.displayPosition(); // "Position: Manager"

この例では、Employeeクラスの#getPositionメソッドがprivateとして定義され、accessPositionメソッドを通じてサブクラスManager内でアクセスされています。

アクセス指定子をメソッドに適用することで、クラス設計がより堅牢になり、内部ロジックを保護しつつ、必要なインターフェースを提供できます。次に、アクセス指定子を使用した場合のエラーハンドリングの方法について見ていきましょう。

エラーハンドリングとアクセス指定子

アクセス指定子を使用することで、クラスのフィールドやメソッドのアクセス範囲を制御できますが、これによって発生する可能性のあるエラーや例外を適切に処理することも重要です。ここでは、アクセス指定子を使用した場合のエラーハンドリングの方法について説明します。

アクセス制限によるエラーの例

アクセス指定子を適切に使用しないと、外部からの不正なアクセスが原因でエラーが発生する可能性があります。以下の例では、privateフィールドへの直接アクセスがエラーを引き起こす様子を示しています。

class SecureDocument {
  #content;

  constructor(content) {
    this.#content = content;
  }

  public getContent() {
    return this.#content;
  }
}

const document = new SecureDocument("Confidential");
console.log(document.getContent()); // "Confidential"
console.log(document.#content); // エラー: Private field '#content' must be declared in an enclosing class

この例では、#contentフィールドに直接アクセスしようとするとエラーが発生します。このようなエラーを回避するために、アクセス指定子を適切に使用し、外部からの直接アクセスを防ぐ必要があります。

メソッド内でのエラーハンドリング

メソッド内でアクセス指定子を使用する場合、適切なエラーハンドリングを実装して、エラーが発生した際に適切に対処できるようにします。

class BankAccount {
  #balance;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  public deposit(amount) {
    try {
      if (amount <= 0) {
        throw new Error("Deposit amount must be positive");
      }
      this.#balance += amount;
      console.log(`Deposited: $${amount}`);
    } catch (error) {
      console.error(error.message);
    }
  }

  public withdraw(amount) {
    try {
      if (amount <= 0) {
        throw new Error("Withdrawal amount must be positive");
      }
      if (amount > this.#balance) {
        throw new Error("Insufficient funds");
      }
      this.#balance -= amount;
      console.log(`Withdrew: $${amount}`);
    } catch (error) {
      console.error(error.message);
    }
  }

  public getBalance() {
    return this.#balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);           // "Deposited: $500"
account.withdraw(2000);         // "Insufficient funds"
account.withdraw(-100);         // "Withdrawal amount must be positive"
console.log(account.getBalance()); // 1500

この例では、depositおよびwithdrawメソッド内でエラーハンドリングを実装し、無効な操作が発生した場合にエラーメッセージを表示しています。

サブクラスでのエラーハンドリング

アクセス指定子を使用したサブクラスでのエラーハンドリングも重要です。親クラスのメソッドをオーバーライドする際に適切なエラーハンドリングを実装することで、クラス間の一貫性を保ちます。

class Employee {
  #name;

  constructor(name) {
    this.#name = name;
  }

  protected getName() {
    return this.#name;
  }

  public displayInfo() {
    console.log(`Employee Name: ${this.#name}`);
  }
}

class Manager extends Employee {
  constructor(name, department) {
    super(name);
    this.department = department;
  }

  public displayInfo() {
    try {
      if (!this.department) {
        throw new Error("Department is required for Manager");
      }
      super.displayInfo();
      console.log(`Department: ${this.department}`);
    } catch (error) {
      console.error(error.message);
    }
  }
}

const manager = new Manager("Alice", "HR");
manager.displayInfo(); // "Employee Name: Alice" "Department: HR"

const faultyManager = new Manager("Bob");
faultyManager.displayInfo(); // "Department is required for Manager"

この例では、Managerクラスが親クラスEmployeedisplayInfoメソッドをオーバーライドし、追加のエラーチェックを実装しています。これにより、サブクラスでのエラーハンドリングが強化されています。

アクセス指定子を使用した場合のエラーハンドリングを適切に実装することで、クラス設計の安全性と堅牢性が向上します。次に、アクセス指定子を使ったクラス設計の演習問題について見ていきましょう。

演習問題:アクセス指定子を使ったクラス設計

ここでは、アクセス指定子を使ったクラス設計の演習問題を提供します。これにより、アクセス指定子の理解を深め、実際のプログラミングでの応用力を高めることができます。

演習問題 1: ライブラリ管理システム

以下の要件を満たすクラス設計を行ってください。

  1. Bookクラスを作成し、title(本のタイトル)、author(著者)、および#copies(蔵書数)というフィールドを持たせます。
  2. Libraryクラスを作成し、複数のBookオブジェクトを管理できるようにします。
  3. Libraryクラスには、次のメソッドを持たせます:
  • addBook(book): 新しい本を追加する。
  • borrowBook(title): 指定されたタイトルの本を1冊借りる。蔵書数が1冊以上ある場合のみ借りられる。
  • returnBook(title): 指定されたタイトルの本を1冊返す。
  • listBooks(): ライブラリにある全ての本のタイトルと著者を表示する。
class Book {
  #copies;

  constructor(title, author, copies) {
    this.title = title;
    this.author = author;
    this.#copies = copies;
  }

  getDetails() {
    return `${this.title} by ${this.author}`;
  }

  borrow() {
    if (this.#copies > 0) {
      this.#copies--;
      return true;
    }
    return false;
  }

  returnBook() {
    this.#copies++;
  }

  getCopies() {
    return this.#copies;
  }
}

class Library {
  constructor() {
    this.books = [];
  }

  addBook(book) {
    this.books.push(book);
  }

  borrowBook(title) {
    const book = this.books.find(book => book.title === title);
    if (book && book.borrow()) {
      console.log(`Borrowed: ${book.getDetails()}`);
    } else {
      console.log(`Sorry, ${title} is not available.`);
    }
  }

  returnBook(title) {
    const book = this.books.find(book => book.title === title);
    if (book) {
      book.returnBook();
      console.log(`Returned: ${book.getDetails()}`);
    } else {
      console.log(`${title} not found in the library.`);
    }
  }

  listBooks() {
    this.books.forEach(book => {
      console.log(`${book.getDetails()} - Copies: ${book.getCopies()}`);
    });
  }
}

// 使用例
const library = new Library();
const book1 = new Book("The Great Gatsby", "F. Scott Fitzgerald", 3);
const book2 = new Book("1984", "George Orwell", 2);

library.addBook(book1);
library.addBook(book2);

library.listBooks();
library.borrowBook("1984");
library.borrowBook("The Great Gatsby");
library.borrowBook("The Great Gatsby");
library.borrowBook("The Great Gatsby");
library.returnBook("1984");
library.listBooks();

演習問題 2: 銀行口座管理システム

以下の要件を満たすクラス設計を行ってください。

  1. BankAccountクラスを作成し、#accountNumber(口座番号)、#balance(残高)、およびowner(口座所有者)というフィールドを持たせます。
  2. BankAccountクラスには、次のメソッドを持たせます:
  • deposit(amount): 指定された金額を預金する。金額は正の数でなければならない。
  • withdraw(amount): 指定された金額を引き出す。残高が足りない場合は引き出しできない。
  • getBalance(): 残高を返す。
  • getAccountDetails(): 口座番号と所有者名を返す。
class BankAccount {
  #accountNumber;
  #balance;
  owner;

  constructor(accountNumber, owner, initialBalance) {
    this.#accountNumber = accountNumber;
    this.owner = owner;
    this.#balance = initialBalance;
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`Deposited: $${amount}`);
    } else {
      console.log('Deposit amount must be positive');
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`Withdrew: $${amount}`);
    } else {
      console.log('Invalid withdrawal amount');
    }
  }

  getBalance() {
    return this.#balance;
  }

  getAccountDetails() {
    return `Account Number: ${this.#accountNumber}, Owner: ${this.owner}`;
  }
}

// 使用例
const account = new BankAccount("123456789", "Alice", 1000);
console.log(account.getAccountDetails());
account.deposit(500);
account.withdraw(300);
console.log(`Balance: $${account.getBalance()}`);
account.withdraw(1500); // Invalid withdrawal amount
console.log(`Balance: $${account.getBalance()}`);

これらの演習問題を通じて、アクセス指定子の使い方とその効果を実際に体験し、クラス設計のスキルを向上させてください。次に、アクセス指定子のベストプラクティスについて見ていきましょう。

アクセス指定子のベストプラクティス

アクセス指定子を適切に使用することで、クラスの設計がより安全で保守しやすくなります。ここでは、アクセス指定子を効果的に活用するためのベストプラクティスを紹介します。

1. デフォルトはprivateに

フィールドやメソッドのアクセス制御を設計する際、デフォルトでprivateに設定することを検討してください。必要に応じてpublicprotectedに変更する方針を取ることで、意図しないアクセスを防ぎやすくなります。

class User {
  #username;
  #password;

  constructor(username, password) {
    this.#username = username;
    this.#password = password;
  }

  public getUsername() {
    return this.#username;
  }

  public checkPassword(password) {
    return this.#password === password;
  }
}

2. カプセル化を徹底する

クラスの内部状態を外部から隠蔽し、カプセル化を徹底します。これにより、内部実装の変更が外部に影響を与えにくくなり、コードの保守性が向上します。

class BankAccount {
  #balance;

  constructor(initialBalance) {
    this.#balance = initialBalance;
  }

  public deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    }
  }

  public withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
    }
  }

  public getBalance() {
    return this.#balance;
  }
}

3. アクセス指定子を明示的に使う

クラス内の全てのフィールドやメソッドに対して、アクセス指定子を明示的に指定する習慣を持つことが重要です。これにより、クラスの設計意図が明確になり、他の開発者がコードを理解しやすくなります。

class Employee {
  #name;
  #position;

  constructor(name, position) {
    this.#name = name;
    this.#position = position;
  }

  public getDetails() {
    return `${this.#name} is a ${this.#position}`;
  }
}

4. 必要に応じてprotectedを使う

継承関係にあるクラス間でフィールドやメソッドを共有する必要がある場合は、protectedを使用してアクセスを制限します。ただし、protectedの使用は最小限にとどめ、できるだけprivateを使用するように心がけます。

class Employee {
  #name;

  constructor(name) {
    this.#name = name;
  }

  protected getName() {
    return this.#name;
  }
}

class Manager extends Employee {
  constructor(name, department) {
    super(name);
    this.department = department;
  }

  public getDetails() {
    return `${this.getName()} is a manager in the ${this.department} department`;
  }
}

5. 例外処理を組み込む

メソッド内で例外処理を適切に実装し、アクセス制御に関連するエラーをキャッチし、適切に対処するようにします。これにより、予期しない動作を防ぎ、コードの堅牢性が向上します。

class SecureFile {
  #content;

  constructor(content) {
    this.#content = content;
  }

  public readContent() {
    try {
      if (!this.#content) {
        throw new Error("No content available");
      }
      return this.#content;
    } catch (error) {
      console.error(error.message);
    }
  }

  public writeContent(newContent) {
    try {
      if (!newContent) {
        throw new Error("Content cannot be empty");
      }
      this.#content = newContent;
    } catch (error) {
      console.error(error.message);
    }
  }
}

アクセス指定子を効果的に使用することで、クラス設計の安全性と保守性が大幅に向上します。これらのベストプラクティスを参考にして、より堅牢で明確なクラス設計を行いましょう。

次に、本記事のまとめとして、アクセス指定子の重要性と使用方法を振り返ります。

まとめ

本記事では、JavaScriptのクラスフィールドとアクセス指定子の使い方について詳しく解説しました。クラスフィールドは、オブジェクトの状態を管理するための基本的な要素であり、アクセス指定子はそのフィールドやメソッドのアクセス範囲を制御するための重要な手段です。

アクセス指定子には、publicprivate、およびJavaScriptでは直接サポートされていないものの、設計により実現可能なprotectedがあります。それぞれのアクセス指定子の使い方や役割を理解することで、クラス設計がより堅牢になり、コードの保守性と安全性が向上します。

さらに、アクセス指定子を活用したエラーハンドリングやベストプラクティスについても学びました。デフォルトでprivateを使用し、必要に応じてpublicprotectedに変更する方針を取ることで、意図しないアクセスを防ぎ、データのカプセル化を徹底することができます。

最後に、演習問題を通じてアクセス指定子の理解を深め、実際の開発に応用できる力を養うことができました。これらの知識を活用して、より安全で保守性の高いJavaScriptプログラムを設計しましょう。

これで、JavaScriptのクラスフィールドのアクセス指定子の使い方に関する解説を終了します。次に取り組むプロジェクトで、これらの概念を活かして効果的なクラス設計を実践してみてください。

コメント

コメントする

目次