KAKUTA TECH BLOG

Blog Article

TypeScriptにおけるオブジェクト指向とは?

はじめに

TypeScript で開発を進めていると、「クラス」「インスタンス」「継承」「カプセル化」など、オブジェクト指向に関係する言葉を目にする機会が増えていきます。しかし、これらの機能を個別に覚えるだけでは、「なぜクラスを使うのか」「なぜデータと処理をまとめるのか」といった本質が分かりにくくなります。

この記事では、まずオブジェクト指向そのものの考え方を理解し、その上で TypeScript のクラスやカプセル化、継承、ポリモーフィズムなどがどのように関係するのかを順番に解説します。

参考にしたリンク

オブジェクト指向を理解すると、なぜコードを整理しやすくなるのか

🔰 一言で言うと「関連するデータと処理をひとまとまりとして扱う考え方」です。

プログラムが小さいうちは、変数や関数を並べて処理を書いても、それほど困ることはありません。しかし、アプリケーションが大きくなると、「このデータはどの処理で使うのか」「この処理を変更すると、どこに影響するのか」が分かりにくくなっていきます。

オブジェクト指向では、現実世界のものをそのまま再現するというより、プログラムの中で扱いたいものを「オブジェクト」として整理するという考え方をします。

例えば、ゲームに「プレイヤー」が登場するとします。プレイヤーには名前や HP などのデータがあり、「攻撃する」「ダメージを受ける」といった処理があります。これらを別々の変数や関数として管理するのではなく、「プレイヤー」というひとまとまりとして管理すると、どのデータと処理が関係しているのかが分かりやすくなります。

この考え方が重要なのは、プログラムが大きくなったときにコードを整理しやすくなるからです。

オブジェクトは「データ」と「そのデータを扱う処理」をまとめたもの

オブジェクト指向を理解するうえで、まず「オブジェクト」が何なのかを理解しておく必要があります。

JavaScript では、オブジェクトを使って複数のデータをひとまとめにできます。例えば、プレイヤーの名前と HP を管理するなら、player.name や player.hp のように、関連するデータを 1 つのオブジェクトとして扱えます。

さらに、オブジェクトの中には関数を持たせることもできます。これによって、「プレイヤーのデータ」と「プレイヤーに関係する処理」を近い場所にまとめられます。

オブジェクトの中で定義された関数をメソッドといいます。

クラスを使ってオブジェクトの設計図を作る

TypeScript では、class を使ってオブジェクトの構造や処理を定義できます。

// player.ts

class Player {
  name: string;
  hp: number;

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

  attack() {
    console.log(`${this.name} が攻撃しました`);
  }
}

この Player クラスでは、プレイヤーが持つ name と hp、そしてプレイヤーが行う attack() をまとめています。

ここで重要なのは、class 自体がプレイヤーそのものではないということです。class は、プレイヤーというオブジェクトを作るための設計図です。

この設計図から実際のプレイヤーを作ることを「インスタンス化」と呼びます。

インスタンスはクラスから作られた実体

// player.ts

const player1 = new Player("勇者", 100);
const player2 = new Player("戦士", 150);

player1.attack();
player2.attack();

new Player() を実行すると、Player クラスをもとに新しいオブジェクトが作られます。

このオブジェクトがインスタンスです。

同じ Player クラスから複数のインスタンスを作れるため、player1 と player2 は同じ構造を持ちながら、それぞれ異なる名前や HP を持つことができます。

つまり、

  • class → オブジェクトを作るための設計図
  • instance → 設計図から作られた実体

という関係です。

カプセル化によって、オブジェクトの状態を安全に管理できる

オブジェクト指向では、データをただまとめるだけでなく、外部から自由に変更されないように管理するという考え方も重要です。

例えば、プレイヤーの HP をどこからでも直接変更できる状態にすると、「負の値になってしまう」「本来許可していない値を設定される」といった問題が起こる可能性があります。

そこで、データへのアクセスを制限し、決められた方法で操作できるようにします。これがカプセル化です。

アクセス修飾子で外部からのアクセスを制限する

TypeScript では、カプセル化を実現するために アクセス修飾子を使用できます。

アクセス修飾子は、クラスのプロパティやメソッドを「どこからアクセスできるか」を指定するためのものです。

代表的なものには public、private、protected があります。

アクセス修飾子

アクセスできる範囲

主な用途

public

クラスの外部からもアクセスできる

外部に公開したいデータや処理

private

定義したクラスの内部からのみアクセスできる

外部から隠したいデータや処理

protected

定義したクラスと、そのクラスを継承したクラスからアクセスできる

継承先でも利用したい内部データや処理

特にカプセル化を理解するうえで重要なのが private です。

private で外部からの直接アクセスを制限する

特にカプセル化を理解するうえで重要なのが private です。

// User.ts
class User {
  private password: string

  constructor(password: string) {
    this.password = password
 } 

 checkPassword(input: string) {
   return this.password === input
  } 
}

このコードでは、password に private を付けています。そのため、クラスの外部から user.password のように直接アクセスすることはできません。

一方で、パスワードを確認するための checkPassword() は外部から利用できます。

つまり、クラスの内部にあるデータを直接操作させるのではなく、クラスが用意したメソッドを通して操作させるようにしています。

これがカプセル化の具体的な例です。

const user = new User("password123")

user.checkPassword("password123") // true

このように、利用する側は「パスワードがどのように保存されているのか」を意識する必要がありません。クラスの内部でデータをどのように管理するかは隠し、外部には必要な操作だけを公開しています。

このようにカプセル化には、データを隠すだけではなく、そのデータを正しい方法で扱わせるという役割があります。

⚠️ 注意点として、アクセス修飾子そのものが「カプセル化」というわけではありません。

カプセル化は「内部のデータや処理を隠し、外部から安全に扱えるようにする」というオブジェクト指向の考え方です。

private や protected などのアクセス修飾子は、その考え方を TypeScript で実現するための手段です。

継承を使うと、共通する処理を再利用できる

ゲームに「プレイヤー」と「敵」が存在するとします。

どちらにも名前や HP があり、攻撃する処理も必要になるかもしれません。しかし、それぞれのクラスに同じ処理を書いてしまうと、コードが重複します。

そこで利用できるのが継承です。 一言で言うと「既存のクラスをもとに、新しいクラスを作る仕組み」です。

extends でクラスを継承する

// character.ts

class Character {
  name: string;
  hp: number;

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

  attack() {
    console.log(`${this.name} が攻撃しました`);
  }
}

class Player extends Character {
  heal() {
    console.log(`${this.name} が回復しました`);
  }
}

Player extends Character とすることで、Player は Character の機能を引き継ぎます。そのため、Player クラスに name や hp、attack() をもう一度書く必要がありません。

このように、共通する部分を親クラスにまとめ、個別の機能を子クラスに追加することで、コードの重複を減らせます。

ポリモーフィズムによって、同じ操作で異なる処理を扱える

継承と関係が深い考え方として、ポリモーフィズムがあります。一言で言うと「同じ操作をしても、オブジェクトによって異なる処理を実行できる仕組み」です。

例えば、「攻撃する」という同じ操作でも、キャラクターによって攻撃方法が異なる場合があります。戦士なら物理攻撃、魔法使いなら魔法攻撃というように、呼び出す処理は同じ attack() でも、中で行われる処理を変えられます。

オーバーライド(メソッド)を上書きする

継承では、親クラスが持っているメソッドを子クラスも利用できます。しかし、親クラスと同じ処理ではなく、子クラスごとに異なる処理をさせたい場合があります。

そのようなときは、子クラスで親クラスと同じ名前のメソッドを定義します。すると、子クラスでは親クラスから受け継いだメソッドの処理を、子クラス独自の処理に置き換えることができます。これを メソッドの上書き(オーバーライド) といいます。

例えば、親クラスの Character に「攻撃する」という attack() メソッドがあるとします。Warrior は剣で攻撃し、Wizard は魔法で攻撃するため、同じ attack() でもクラスによって処理を変えたいとします。

この場合、Warrior と Wizard でそれぞれ attack() を定義すると、親クラスの attack() を上書きできます。

// character.ts

class Character {
  attack() {
    console.log("通常攻撃");
  }
}

class Warrior extends Character {
  attack() {
    console.log("剣で攻撃");
  }
}

class Wizard extends Character {
  attack() {
    console.log("魔法で攻撃");
  }
}

const characters = [
  new Warrior(),
  new Wizard(),
];

characters.forEach((character) => {
  character.attack();
});

このコードでは、Warrior と Wizard は Character を継承しています。

親クラスの Character には、attack() が定義されています。何も上書きしなければ、Warrior と Wizard もこの attack() をそのまま利用することになります。

しかし、Warrior では attack() をもう一度定義して、「剣で攻撃」と表示する処理に置き換えています。同じように、Wizard では「魔法で攻撃」と表示する処理に置き換えています。

そのため、character.attack() という同じメソッドを呼び出しているにもかかわらず、実際に実行される処理はオブジェクトによって異なります。

この「同じ操作をしているのに、オブジェクトによって異なる処理を実行できる」という性質が、ポリモーフィズムにつながります。

インターフェースは、オブジェクトの共通ルールを定義するために使える

TypeScript では、interface を使ってオブジェクトが持つべき構造を定義できます。

インターフェースはクラスそのものではありません。「このオブジェクトは、このようなデータや処理を持ってください」というルールを定義するものです。

オブジェクト指向では、複数のクラスに共通するルールを定義したい場合などに利用できます。

// character.ts

interface Character {
  name: string;
  attack(): void;
}

class Warrior implements Character {
  name: string;

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

  attack() {
    console.log("剣で攻撃");
  }
}

class Wizard implements Character {
  name: string;

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

  attack() {
    console.log("魔法で攻撃");
  }
}

Character インターフェースでは、name と attack() を持つことをルールとして定義しています。Warrior と Wizard は、それぞれ異なる処理を持っていますが、どちらも Character のルールに従っています。

このように、インターフェースを使うことで「何を持っている必要があるか」という共通ルールと、「実際にどう処理するか」という具体的な実装を分離できます。

TypeScript でオブジェクト指向を使う意味

ここまで見てきたように、TypeScript のオブジェクト指向では、単にクラスを書くことが目的ではありません。

大切なのは、複雑になったプログラムを役割ごとに整理し、変更しやすくすることです。

例えば、プレイヤーに関するデータと処理を Player にまとめておけば、プレイヤーに関する変更が発生したときに、どこを修正すればよいのか分かりやすくなります。

また、共通する処理を継承によってまとめたり、インターフェースによって共通のルールを定義したりすることで、複数のオブジェクトを統一的に扱いやすくなります。

ただし、すべてのコードでクラスを使えばよいわけではありません。

小さな処理まで無理にクラス化すると、かえってコードが複雑になることがあります。

⚠️ 注意・気をつけること

「オブジェクト指向だからクラスを使う」のではなく、コードを整理するうえでクラスや継承などが有効な場合に使うと考えることが重要です。

項目

意味

オブジェクト指向

データと処理をひとまとまりとして扱い、コードを整理する考え方

クラス

オブジェクトを作るための設計図

インスタンス

クラスから作られた実体

カプセル化

オブジェクト内部の状態を外部から勝手に操作されないようにする考え方

継承

既存のクラスをもとに新しいクラスを作る仕組み

ポリモーフィズム

同じ操作でもオブジェクトによって異なる処理を実行する考え方

interface

オブジェクトやクラスが従う共通ルールを定義する仕組み

オブジェクト指向を理解するときは、class や extends などの構文を暗記するだけではなく、「なぜデータと処理をまとめるのか」「なぜ共通部分を切り出すのか」という目的から考えることが重要です。