TypeScript | 関数・クラス・ジェネリクス:クラス設計 – public / private / protected

TypeScript TypeScript
スポンサーリンク

ゴール:「どこまで外に見せるか」を自分でコントロールできるようになる

public / private / protected は、
「クラスの中身をどこまで外に見せるか」を決めるスイッチです。

ここをなんとなくで書くと、

  • 触ってほしくないところを外からいじられる
  • クラスの使い方がバラバラになる
  • 後から仕様変更しづらくなる

みたいな“じわじわ効く痛み”が出てきます。

逆に、ここを意識して設計できると、

「このクラスは、ここだけ見て使ってね」
という“きれいな顔”を作れるようになります。

順番に、かみ砕いていきます。


public:みんなに見せる「表の顔」

public は「どこからでもアクセスOK」

public は「外から自由に触っていいよ」という意味です。
何も書かなかった場合も public 扱いになります。

class User {
  public id: number;
  public name: string;

  public constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  public greet(): void {
    console.log(`こんにちは、${this.name}です`);
  }
}

const u = new User(1, "Taro");
console.log(u.id);   // OK
u.name = "Jiro";     // OK
u.greet();           // OK
TypeScript

「このクラスを使う人に、ここは自由に触ってほしい」
という場所には public を付けます。

「とりあえず全部 public」は危険

初心者がやりがちなのが、

class User {
  id: number;
  name: string;
  isDeleted: boolean;

  // いろんなメソッド…
}
TypeScript

とりあえず何も付けずに書いて、
結果的に「全部 public」になってしまうパターンです。

これをやると、

  • isDeleted を外から勝手に書き換えられる
  • 内部の都合で持っているメソッドまで外から呼べてしまう

など、「クラスの中身が丸裸」になります。

public は「見せたいものだけ」に絞る
という感覚を持てるようになると、一気に設計が良くなります。


private:クラスの中だけで使う「裏側の仕組み」

private は「クラスの外からは触っちゃダメ」

private は「このクラスの中からしか触れない」という意味です。

class User {
  constructor(
    public name: string,
    private passwordHash: string
  ) {}

  public checkPassword(raw: string): boolean {
    const hash = this.hash(raw);
    return hash === this.passwordHash;
  }

  private hash(raw: string): string {
    // 実際にはもっとちゃんとしたハッシュ処理
    return `hashed:${raw}`;
  }
}

const u = new User("Taro", "hashed:secret");

// u.passwordHash;      // エラー:private
// u.hash("secret");    // エラー:private

u.checkPassword("secret"); // OK
TypeScript

ここでのポイントは、

  • passwordHash は外から見えない
  • hash メソッドも外から呼べない
  • 外からできるのは checkPassword を呼ぶことだけ

という状態になっていることです。

「外から触らせたくないもの」は全部 private にする

クラスの中には、

  • ビジネスルール上、外から変えられたら困る値
  • 内部実装の都合で存在するだけのメソッド
  • 将来、実装を変えたいかもしれない部分

がたくさん出てきます。

そういうものは、積極的に private にします。

class Order {
  private items: { name: string; price: number }[] = [];

  public addItem(name: string, price: number): void {
    this.items.push({ name, price });
  }

  public getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}
TypeScript

もし items が public だったら、

order.items.push({ name: "変な商品", price: -9999 });
TypeScript

みたいなことが外からできてしまいます。

「このプロパティやメソッドを外から触られたら困るか?」
と自分に聞いて、「困る」と思ったら private にする、
これだけでも設計がかなり締まります。


protected:継承したクラスには見せる「家族だけの共有」

protected は「外からはダメ、子クラスからはOK」

protected は、
「クラスの外からは見えないけど、継承したクラスからは見える」
という中間的な立場です。

class Animal {
  constructor(
    protected name: string
  ) {}

  protected move(distance: number): void {
    console.log(`${this.name} moved ${distance}m`);
  }
}

class Dog extends Animal {
  public bark(): void {
    console.log(`${this.name} がワンと鳴いた`); // OK(protected)
    this.move(5);                                // OK(protected)
  }
}

const d = new Dog("Pochi");
// d.name;   // エラー:外からは見えない
// d.move(5); // エラー:外からは見えない
d.bark();    // OK
TypeScript

イメージとしては、

  • public:誰でも見ていい
  • protected:家族(継承したクラス)だけ見ていい
  • private:本人(そのクラス自身)しか見ちゃダメ

という感じです。

継承前提のクラスで効いてくる

protected が本領発揮するのは、
「継承されることを前提にしたクラス」です。

abstract class Shape {
  constructor(
    protected color: string
  ) {}

  abstract area(): number;

  protected logArea(): void {
    console.log(`色: ${this.color}, 面積: ${this.area()}`);
  }
}

class Circle extends Shape {
  constructor(
    color: string,
    private radius: number
  ) {
    super(color);
  }

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

  public debug(): void {
    this.logArea(); // protected メソッドを子クラスから利用
  }
}
TypeScript

logArea は外から直接呼ばせたくないけれど、
子クラスからは使わせたい、というときに protected が使えます。


どう使い分ける?実務でのざっくり指針

まず「全部 private だと思って」必要なものだけ public にする

一番おすすめの考え方はこれです。

「最初は全部 private だと思って、
外から必要なものだけ public に“昇格”させる」

いきなり public でベタッと書くのではなく、

  • このクラスを使う人は、どこに触れれば用が足りる?
  • 逆に、触れてほしくないところはどこ?

を考えて、「表の顔(public)」を絞り込んでいきます。

class User {
  // 本当は全部 private から考える
  private id: number;
  private name: string;

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

  // 外から必要な操作だけ public にする
  public rename(newName: string): void {
    this.name = newName;
  }

  public getDisplayName(): string {
    return `ID:${this.id} / ${this.name}`;
  }
}
TypeScript

こうすると、

  • 外からできることが明確になる
  • 内部の実装を変えても、public な部分さえ守れば壊れない

という「変更に強いクラス」になっていきます。

「テストしたいから public にする」は危険

ありがちな罠がこれです。

「この内部メソッドもテストしたいから、とりあえず public にしちゃおう」

class Something {
  public internalLogic(): number {
    // 本当は外から呼ばせたくない
  }
}
TypeScript

テストのために public を増やすと、

  • 本来隠したかった内部実装が API として露出する
  • そのメソッドを他のコードからも呼ばれ始める
  • 後から変えづらくなる

という悪循環に入ります。

テストしたい内部ロジックは、
別の純粋な関数としてクラスの外に切り出す
という手もあります。

function calcSomething(x: number): number {
  // テストしやすい純粋関数
}

class Something {
  public do(): number {
    return calcSomething(42);
  }
}
TypeScript

アクセス修飾子は「テストの都合」ではなく、
「クラスの設計としてどうあるべきか」で決めるのが大事です。


まとめ:public / private / protected を自分の言葉で言うと

最後に、あなた自身の言葉でこう整理してみてください。

  • public:
    「このクラスを使う人に“ここは触っていいよ”と約束する場所」
    クラスの“顔”になる部分。最小限に絞る。
  • private:
    「クラスの中だけで完結させたい裏側の仕組み」
    外から触られたくない状態・メソッドは全部ここに閉じ込める。
  • protected:
    「外からは隠したいけど、子クラスには見せたい共有部分」
    継承前提の設計で効いてくる。

コードを書いていて、
プロパティやメソッドを1つ追加するときに、
毎回一瞬だけでいいので、

「これは本当に public でいい?」
「private にできない?」
「継承するなら protected のほうが自然?」

と自分に問いかけてみてください。

その小さな習慣が、
クラスを「なんでも public な箱」から、
“外から見たときにスッと理解できる、きれいなインターフェース”
へと育てていきます。

タイトルとURLをコピーしました