TypeScriptのアクセス指定子(public, private, protected)を徹底解説:使い方と活用例

TypeScriptのクラスを使用する際、アクセス指定子(public, private, protected)は非常に重要な役割を果たします。これらの指定子は、クラスのメンバー(プロパティやメソッド)がどこからアクセス可能かを制御するための機能です。適切なアクセス指定子を使うことで、クラス内部のデータを保護し、外部からの誤った操作を防ぐことができ、より安全で管理しやすいコードを書くことが可能になります。本記事では、TypeScriptのアクセス指定子の違いとその効果的な使い方について詳しく解説します。

目次

アクセス指定子とは何か

アクセス指定子は、クラスのプロパティやメソッドに対して、どの範囲からアクセスできるかを指定する機能です。TypeScriptでは、publicprivateprotectedの3つのアクセス指定子があり、これらを使うことでクラスのメンバーの可視性を制御します。

クラスにおけるアクセスの制御

アクセス指定子を使用することで、クラスの外部から特定のプロパティやメソッドに直接アクセスできるかどうかを決定できます。これにより、クラス内部のデータを保護したり、特定の動作だけを公開することが可能になります。

アクセス指定子がない場合の挙動

TypeScriptでは、アクセス指定子を明示しない場合、publicがデフォルトで適用されます。つまり、すべてのクラスメンバーは外部からアクセス可能となりますが、これは必ずしも最適な設計ではありません。適切な指定子を使うことで、クラスの意図を明確に伝えることができます。

publicアクセス指定子の使い方

publicアクセス指定子は、クラスのプロパティやメソッドを外部から自由にアクセスできる状態にします。TypeScriptにおいて、publicはデフォルトのアクセス指定子なので、特に指定がなくてもクラスメンバーはすべてpublicとして扱われます。

publicの基本的な使い方

publicは、クラスの外部やインスタンスからアクセスされることが想定されるメンバーに使用します。たとえば、外部から値を取得したり設定したりするメソッドや、共有すべきデータを公開する際に使用されます。

class Person {
  public name: string;

  constructor(name: string) {
    this.name = name;
  }

  public greet(): void {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const john = new Person("John");
john.greet(); // "Hello, my name is John"
console.log(john.name); // "John"

publicが有効なシナリオ

publicを使用するケースとして、例えば、クラスのデータを外部から簡単に取得・操作できる場合が挙げられます。APIを使ってデータを取得したり、ユーザーインターフェイスとやり取りするような場合に、プロパティやメソッドをpublicに設定すると便利です。

publicを適切に使うことで、クラスの外部からのアクセスを効率化し、柔軟なコードの設計が可能になります。

privateアクセス指定子の使い方

privateアクセス指定子は、クラス内部でのみアクセス可能なプロパティやメソッドを定義する際に使用します。privateで指定されたメンバーは、クラスの外部から直接アクセスすることができません。これにより、外部からの誤ったアクセスやデータの不正な変更を防ぐことができます。

privateの基本的な使い方

privateは、クラスの内部でのみ利用されるべきデータやメソッドに使用します。例えば、内部でのみ計算を行うメソッドや、外部から直接アクセスするべきでないプロパティをprivateにすることで、クラスのデータをカプセル化できます。

class BankAccount {
  private balance: number;

  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }

  public deposit(amount: number): void {
    this.balance += amount;
  }

  public getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// console.log(account.balance); // エラー: balanceはprivateのためアクセスできない

privateを使うメリット

private指定子を使うことで、クラス内部の実装を隠蔽し、外部から直接操作されることを防ぎます。これにより、クラス内部のデータが予期しない状態になるのを防ぎ、バグや予期しない動作を減少させます。また、クラスの利用者にとって、外部から操作可能な範囲が明確になるため、コードの保守性も向上します。

privateは、クラスの設計において重要な役割を果たし、堅牢で予測可能なコードを作成するために不可欠な要素です。

protectedアクセス指定子の使い方

protectedアクセス指定子は、クラス自身とそのサブクラス(継承されたクラス)からのみアクセス可能なプロパティやメソッドを定義する際に使用します。protected指定子は、クラスの外部からは直接アクセスできませんが、継承されたクラス内では自由に利用することが可能です。

protectedの基本的な使い方

protectedは、基底クラス内で定義され、派生クラスで再利用されるプロパティやメソッドに使用します。これにより、サブクラスでの振る舞いをカスタマイズしつつ、クラス外部からはデータを保護することができます。

class Animal {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

  protected makeSound(): void {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }

  public bark(): void {
    this.makeSound();
    console.log(`${this.name} barks`);
  }
}

const dog = new Dog("Rex");
dog.bark(); // "Rex makes a sound" and "Rex barks"
// console.log(dog.name); // エラー: nameはprotectedのためアクセスできない

protectedを使うメリット

protectedを使用することで、基底クラス内のロジックやデータを継承したクラスで再利用できる柔軟性を持たせつつ、クラス外部からはこれらのメンバーへのアクセスを制限することが可能です。これにより、継承を通じてクラスを拡張する際、重要なデータやロジックを保護しながらも、サブクラスでのカスタマイズを可能にします。

例えば、特定のメソッドやプロパティをサブクラス内でのみ使う必要がある場合にprotectedを使うことで、クラスの安全性と拡張性を確保できます。

アクセス指定子の違いを比較

TypeScriptにおけるpublicprivateprotectedの3つのアクセス指定子は、それぞれ異なる範囲でのアクセスを許可し、クラスの設計において重要な役割を果たします。ここでは、これらの指定子の違いをコード例を交えて詳しく比較します。

アクセス範囲の違い

アクセス指定子は、クラスのメンバーに対してどの範囲からアクセスできるかを制御します。

  • public: クラスの外部からも自由にアクセス可能。
  • private: クラスの内部からのみアクセス可能。クラス外部やサブクラスからはアクセスできない。
  • protected: クラス内部およびサブクラスからのみアクセス可能。クラス外部からはアクセスできない。

以下のコードは、各アクセス指定子の動作を示しています。

class BaseClass {
  public publicMember: string = "Public";
  private privateMember: string = "Private";
  protected protectedMember: string = "Protected";

  public showMembers(): void {
    console.log(this.publicMember);  // OK
    console.log(this.privateMember); // OK
    console.log(this.protectedMember); // OK
  }
}

class DerivedClass extends BaseClass {
  public showProtectedMember(): void {
    console.log(this.protectedMember); // OK (サブクラスからアクセス可能)
  }

  public showPrivateMember(): void {
    // console.log(this.privateMember); // エラー: privateメンバーはサブクラスからアクセス不可
  }
}

const baseInstance = new BaseClass();
console.log(baseInstance.publicMember); // OK (publicはクラス外部からアクセス可能)
// console.log(baseInstance.privateMember); // エラー: privateはクラス外部からアクセス不可
// console.log(baseInstance.protectedMember); // エラー: protectedはクラス外部からアクセス不可

public、private、protectedの使い分け

publicは、外部からも利用されることを前提としたメンバーに使用します。privateは、クラスの内部のみで使用される重要なデータやメソッドに使用し、外部やサブクラスからの不正なアクセスを防ぎます。protectedは、サブクラスに継承されるが、外部には公開しないメンバーに使用し、継承の際に利用可能な範囲を制限する役割を果たします。

これらの指定子を使い分けることで、クラスの設計に柔軟性を持たせながら、必要に応じて適切にデータやメソッドを保護できます。

アクセス指定子の組み合わせ

TypeScriptでは、アクセス指定子を他のTypeScriptの機能と組み合わせることで、さらに強力で柔軟なコード設計が可能になります。ここでは、アクセス指定子とコンストラクタ、getter/setter、そしてインターフェースを組み合わせた使い方について解説します。

コンストラクタとアクセス指定子の組み合わせ

TypeScriptでは、コンストラクタのパラメータに直接アクセス指定子を指定することができ、シンプルかつ明確なクラス定義が可能です。この方法を使うことで、コンストラクタで初期化すると同時にプロパティの可視性を制御することができます。

class Person {
  constructor(public name: string, private age: number) {}

  public getAge(): number {
    return this.age;
  }
}

const john = new Person("John", 30);
console.log(john.name); // OK: "John"
// console.log(john.age); // エラー: ageはprivateのため外部からアクセス不可

この例では、namepublicとしてコンストラクタ内で定義されており、外部からアクセス可能ですが、ageprivateとして定義されているため、外部から直接アクセスすることはできません。

getter/setterとアクセス指定子の組み合わせ

TypeScriptでは、getterとsetterを使って、privateprotectedのプロパティに対する制御されたアクセスを提供できます。これにより、外部からのアクセスを制限しながら、必要に応じてプロパティの読み取りや書き込みが可能です。

class Employee {
  private _salary: number;

  constructor(salary: number) {
    this._salary = salary;
  }

  public get salary(): number {
    return this._salary;
  }

  public set salary(value: number) {
    if (value > 0) {
      this._salary = value;
    } else {
      console.log("Salary must be positive");
    }
  }
}

const employee = new Employee(50000);
console.log(employee.salary); // OK: 50000
employee.salary = 55000; // OK
employee.salary = -1000; // エラー: "Salary must be positive"

このように、getter/setterを使うことで、privateプロパティに対して外部から直接アクセスさせることなく、制御された方法で読み取りや書き込みを行うことができます。

インターフェースとアクセス指定子の組み合わせ

インターフェースでは、アクセス指定子を使用することはできませんが、インターフェースを通じて定義されたプロパティやメソッドは、クラスに実装された際にアクセス指定子によって制御されます。インターフェースを使うことで、クラスの外部からどのプロパティやメソッドが利用可能かを柔軟に設計できます。

interface User {
  name: string;
  getName(): string;
}

class Admin implements User {
  public name: string;
  private role: string;

  constructor(name: string, role: string) {
    this.name = name;
    this.role = role;
  }

  public getName(): string {
    return this.name;
  }

  private getRole(): string {
    return this.role;
  }
}

const admin = new Admin("Alice", "Administrator");
console.log(admin.getName()); // OK: "Alice"
// console.log(admin.getRole()); // エラー: getRoleはprivateのためアクセス不可

インターフェースにより、namegetName()メソッドはクラスの外部から利用可能ですが、privateとして実装されたgetRole()メソッドは外部からアクセスできません。

アクセス指定子を組み合わせた利点

アクセス指定子と他のTypeScript機能を組み合わせることで、クラスのカプセル化を維持しながら、柔軟かつ安全なコード設計を実現できます。これにより、クラスのメンバーを外部に適切に公開し、必要な範囲でのアクセス制御を強化できます。

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

TypeScriptにおいて、publicprivateprotectedのアクセス指定子を適切に使うことは、コードの可読性やメンテナンス性、そして安全性を向上させるために重要です。ここでは、アクセス指定子の効果的な使用法や、プロジェクトでのベストプラクティスについて解説します。

必要な範囲だけを公開する

アクセス指定子を選ぶ際の基本的なルールは、「必要最小限の公開」を心がけることです。クラスのメンバーは、基本的には外部からのアクセスを制限し、必要な場合だけpublicに設定するべきです。これは、外部からの誤った操作や、データの不正な変更を防ぐためです。

class User {
  private password: string;
  public username: string;

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

  public authenticate(inputPassword: string): boolean {
    return this.password === inputPassword;
  }
}

この例では、passwordフィールドはprivateに設定されており、外部から直接アクセスすることはできませんが、authenticateメソッドを通して認証処理を行うことができます。このように、重要なデータは内部に隠蔽し、必要なインターフェースのみをpublicにします。

外部APIやライブラリとの統合

外部APIやライブラリと統合する場合、publicアクセス指定子を使って、ユーザーに対して明確で簡単なインターフェースを提供します。ただし、内部のロジックやデータの処理部分はprivateprotectedを使って適切に隠蔽します。

class ApiClient {
  private apiKey: string;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  public fetchData(endpoint: string): void {
    // 外部APIからデータを取得する処理
    console.log(`Fetching data from ${endpoint} with API key: ${this.apiKey}`);
  }
}

この例では、apiKeyは外部からアクセスされるべきではないためprivateに設定されていますが、fetchDataメソッドを通じてデータを取得するインターフェースはpublicに設定されています。

カプセル化を維持するためのprotectedの使用

protectedを使うことで、クラスのメンバーがサブクラスで再利用できるようにしつつ、外部からは直接アクセスされないように保護することができます。特に、継承を多用するデザインパターンでは、protectedを使うことで、親クラスの実装をサブクラスで効率的に活用し、コードの重複を防ぐことができます。

class Vehicle {
  protected speed: number;

  constructor(speed: number) {
    this.speed = speed;
  }

  protected accelerate(): void {
    this.speed += 10;
  }
}

class Car extends Vehicle {
  public drive(): void {
    this.accelerate();
    console.log(`Driving at speed ${this.speed}`);
  }
}

const car = new Car(50);
car.drive(); // "Driving at speed 60"

この例では、accelerateメソッドはprotectedに設定されており、サブクラスでのみアクセス可能です。これにより、外部からの不正な操作を防ぎつつ、クラスの継承を効果的に利用できます。

チームでのコーディング規約を明確にする

アクセス指定子の使用法は、プロジェクト全体やチームでのコーディング規約として統一しておくと、保守性が向上します。特に大規模なプロジェクトでは、どのプロパティやメソッドが外部に公開されているかを明確にしておくことで、誤った操作や予期しないエラーを防ぐことができます。

適切なアクセス指定子を使い分けることは、コードのセキュリティと可読性を高めるために不可欠な要素です。アクセス指定子を正しく理解し、プロジェクトの設計に組み込むことで、より堅牢で保守性の高いコードベースを構築することが可能です。

実践的な応用例

アクセス指定子を理解することは重要ですが、実際にどのように使うかを把握するためには、具体的な応用例を見るのが効果的です。ここでは、クラス設計においてpublicprivateprotectedのアクセス指定子を活用した実践的なシナリオを紹介します。

ユーザー認証システムにおけるアクセス指定子の使用

ユーザー認証システムを構築する際、データの保護が非常に重要です。例えば、ユーザーのパスワードは外部から直接アクセスされるべきではありませんが、ユーザー名やログインステータスは公開されても問題ありません。このような状況でアクセス指定子を活用することで、セキュアで管理しやすいシステムを設計できます。

class User {
  public username: string;
  private password: string;
  protected loggedIn: boolean = false;

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

  public login(inputPassword: string): boolean {
    if (this.password === inputPassword) {
      this.loggedIn = true;
      console.log(`${this.username} has logged in.`);
      return true;
    } else {
      console.log("Incorrect password.");
      return false;
    }
  }

  public logout(): void {
    this.loggedIn = false;
    console.log(`${this.username} has logged out.`);
  }
}

class AdminUser extends User {
  public isAdmin: boolean = true;

  public deleteUser(user: User): void {
    if (this.loggedIn) {
      console.log(`Admin ${this.username} deleted user ${user.username}`);
    } else {
      console.log(`Admin ${this.username} is not logged in and cannot delete users.`);
    }
  }
}

const user = new User("alice", "password123");
user.login("password123"); // alice has logged in.
console.log(user.username); // "alice"
// console.log(user.password); // エラー: passwordはprivateのためアクセス不可

const admin = new AdminUser("admin", "adminpass");
admin.login("adminpass"); // admin has logged in.
admin.deleteUser(user); // Admin admin deleted user alice

この例では、passwordprivateとして設定されており、クラス外部から直接アクセスすることはできませんが、loginメソッドを通じて検証が可能です。また、loggedInprotectedとして設定されており、サブクラス(ここではAdminUser)でアクセスできますが、クラス外部からはアクセスできません。

銀行口座システムでのデータ保護

次に、銀行口座のシステムを例に、アクセス指定子を利用して口座の残高などの機密データを保護しつつ、安全な方法でデータ操作を行う方法を示します。

class BankAccount {
  private balance: number;
  public accountNumber: string;

  constructor(accountNumber: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
  }

  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
      console.log(`Deposited ${amount}, new balance: ${this.getBalance()}`);
    } else {
      console.log("Deposit amount must be positive.");
    }
  }

  public withdraw(amount: number): void {
    if (amount > 0 && amount <= this.balance) {
      this.balance -= amount;
      console.log(`Withdrew ${amount}, new balance: ${this.getBalance()}`);
    } else {
      console.log("Invalid withdraw amount.");
    }
  }

  public getBalance(): number {
    return this.balance;
  }
}

const myAccount = new BankAccount("123456789", 1000);
myAccount.deposit(500); // Deposited 500, new balance: 1500
myAccount.withdraw(300); // Withdrew 300, new balance: 1200
console.log(myAccount.accountNumber); // OK: 123456789
// console.log(myAccount.balance); // エラー: balanceはprivateのためアクセス不可

この銀行口座システムでは、balanceプロパティをprivateにすることで、クラス外部から直接アクセスされることを防ぎ、depositwithdrawメソッドを通じてのみ安全に操作が可能になっています。getBalanceメソッドで残高を取得できますが、直接変更はできません。

アクセス指定子を使った継承のカスタマイズ

継承を使ったシステムで、protectedを利用して親クラスの内部データをサブクラスで操作する例を示します。サブクラスでは親クラスのprotectedメンバーにアクセスできますが、外部からのアクセスは制限されます。

class Shape {
  protected area: number = 0;

  public getArea(): number {
    return this.area;
  }
}

class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super();
    this.calculateArea();
  }

  private calculateArea(): void {
    this.area = this.width * this.height;
  }
}

const rectangle = new Rectangle(10, 5);
console.log(rectangle.getArea()); // 50
// console.log(rectangle.area); // エラー: areaはprotectedのためアクセス不可

この例では、areaprotectedとして親クラスShapeに定義されていますが、サブクラスRectangleで計算されています。これにより、外部から直接areaにアクセスすることはできませんが、サブクラス内で安全に操作することが可能です。

まとめ

これらの実践的な例から分かるように、アクセス指定子を適切に使うことで、セキュリティや安全性を確保しつつ、クラスの拡張やメンテナンスがしやすい柔軟な設計が可能になります。実際のプロジェクトでも、データを保護しつつ、必要なインターフェースだけを公開することで、より信頼性の高いコードを構築することができます。

アクセス指定子を使った演習問題

TypeScriptのアクセス指定子(public, private, protected)の理解を深めるために、いくつかの演習問題を提供します。これらの問題を解くことで、アクセス指定子の使い方や、クラス設計における効果的なアクセス制御を実践的に学ぶことができます。

演習問題1: クラスのカプセル化

以下の要件を満たすPersonクラスを作成してください。

  1. name(名前)はpublicプロパティとします。
  2. age(年齢)はprivateプロパティとし、直接アクセスできないようにします。
  3. getAgeメソッドで年齢を取得できるようにします。
  4. 年齢を設定するためのsetAgeメソッドを作成し、0以上100以下の値のみ設定できるように制限します。
class Person {
  public name: string;
  private age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.setAge(age);
  }

  public getAge(): number {
    return this.age;
  }

  public setAge(age: number): void {
    if (age >= 0 && age <= 100) {
      this.age = age;
    } else {
      console.log("Invalid age. Please enter a value between 0 and 100.");
    }
  }
}

// テスト
const person = new Person("Alice", 30);
console.log(person.name); // Alice
console.log(person.getAge()); // 30
person.setAge(110); // Invalid age. Please enter a value between 0 and 100.
console.log(person.getAge()); // 30 (変更されない)

演習問題2: 継承とアクセス制御

以下の要件を満たすAnimalクラスとその派生クラスDogを作成してください。

  1. name(名前)はprotectedプロパティとします。
  2. Animalクラスに、speakメソッドを定義し、動物が音を出す動作を実装しますが、具体的な音声はサブクラスで実装します。
  3. Dogクラスでbarkメソッドを作成し、犬の鳴き声を出す動作を実装します。
class Animal {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

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

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }

  public bark(): void {
    console.log(`${this.name} barks: Woof Woof!`);
  }
}

// テスト
const dog = new Dog("Rex");
dog.speak(); // Rex makes a sound.
dog.bark(); // Rex barks: Woof Woof!

演習問題3: クラスの内部データの保護

次に、銀行口座をシミュレートするBankAccountクラスを作成してください。

  1. balance(残高)はprivateプロパティとします。
  2. depositメソッドで入金処理を行い、0より大きい金額を追加します。
  3. withdrawメソッドで出金処理を行い、現在の残高が出金額より多い場合のみ出金を許可します。
  4. getBalanceメソッドで残高を取得できるようにします。
class BankAccount {
  private balance: number;

  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }

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

  public withdraw(amount: number): void {
    if (amount > 0 && amount <= this.balance) {
      this.balance -= amount;
      console.log(`Withdrew ${amount}. New balance: ${this.balance}`);
    } else {
      console.log("Invalid withdraw amount or insufficient balance.");
    }
  }

  public getBalance(): number {
    return this.balance;
  }
}

// テスト
const account = new BankAccount(1000);
account.deposit(500); // Deposited 500. New balance: 1500
account.withdraw(200); // Withdrew 200. New balance: 1300
account.withdraw(2000); // Invalid withdraw amount or insufficient balance.
console.log(account.getBalance()); // 1300

演習問題4: クラスの継承とアクセスの制限

Shapeクラスを基底クラスとして、派生クラスCircleを作成してください。

  1. radius(半径)はprotectedプロパティとします。
  2. ShapeクラスでgetAreaメソッドを定義し、サブクラスでエリアの計算方法を実装します。
  3. Circleクラスで円の面積を計算し、getAreaメソッドをオーバーライドして実装します。
class Shape {
  protected radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  public getArea(): number {
    return 0; // サブクラスでオーバーライド
  }
}

class Circle extends Shape {
  constructor(radius: number) {
    super(radius);
  }

  public getArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

// テスト
const circle = new Circle(5);
console.log(circle.getArea()); // 78.53981633974483

まとめ

これらの演習問題を通じて、アクセス指定子を使ってクラスのデータやメソッドを適切に制御する方法を学びました。演習を行うことで、アクセス指定子の役割とその実際の活用法について、より深い理解を得ることができます。

まとめ

本記事では、TypeScriptにおけるアクセス指定子(publicprivateprotected)の違いとその使い方について詳しく解説しました。これらの指定子を効果的に使うことで、クラスのデータ保護や安全な設計、継承を活用した柔軟なコードの構築が可能になります。特に、publicは外部公開用、privateは完全なデータ隠蔽、protectedは継承クラス向けのアクセス制御に利用され、これらを適切に使い分けることで、堅牢かつ保守性の高いプログラムを実現できます。

コメント

コメントする

目次