これまでの学習で、TypeScriptの基本的な型システムや関数について理解を深めてきました。今回は、より大規模で保守しやすいアプリケーションを構築するために不可欠な概念である「オブジェクト指向プログラミング(OOP)」に焦点を当てます。
オブジェクト指向プログラミングは、データとそれを操作する処理をひとまとまりにして扱うことで、プログラムの再利用性、拡張性、保守性を高める設計手法です。TypeScriptはJavaScriptに強力な型システムをもたらすだけでなく、このオブジェクト指向プログラミングを効果的に実現するための機能も豊富に提供しています。
本回では、TypeScriptにおけるオブジェクト指向プログラミングの核となる要素である「クラス」「継承」「インターフェース」について、具体的なコード例を交えながら学び、実際に手を動かしながらその概念を習得することを目指します。
エンジニアオブジェクト指向プログラミングは、大規模なアプリ開発でコードを整理しやすくする考え方です。TypeScriptでどう実装するか、見ていきましょう!
1. クラスの基本
クラスは、オブジェクト指向プログラミングにおける「設計図」のようなものです。この設計図から具体的な「オブジェクト(インスタンス)」を生成します。クラスはデータ(プロパティ)と、そのデータを操作する機能(メソッド)をひとまとまりにして定義します。
クラスの定義とインスタンス化
最も基本的なクラスの定義方法を見てみましょう。
class Car {
// プロパティ(データ)
brand: string;
model: string;
year: number;
// コンストラクタ: オブジェクトが生成されるときに実行される特別なメソッド
constructor(brand: string, model: string, year: number) {
this.brand = brand;
this.model = model;
this.year = year;
}
// メソッド(機能)
drive(): void {
console.log(`${this.year}年製 ${this.brand} ${this.model} が走行しています。`);
}
getCarInfo(): string {
return `ブランド: ${this.brand}, モデル: ${this.model}, 年式: ${this.year}`;
}
}
// クラスからインスタンスを生成
const myCar = new Car("トヨタ", "プリウス", 2020);
const anotherCar = new Car("ホンダ", "フィット", 2018);
// インスタンスのメソッドを呼び出す
myCar.drive();
console.log(myCar.getCarInfo());
anotherCar.drive();
console.log(anotherCar.getCarInfo());
上記の例では、Carクラスが定義されています。
brand,model,yearは、Carオブジェクトが持つデータ(プロパティ)です。constructorは、new Car(...)として新しいオブジェクトが作られる際に呼び出され、プロパティの初期値を設定します。thisキーワードは、現在操作しているオブジェクト自身を指します。driveやgetCarInfoは、Carオブジェクトができる操作(メソッド)です。
アクセス修飾子
クラスのプロパティやメソッドには、外部からのアクセスを制御するための「アクセス修飾子」を付けることができます。
public: どこからでもアクセス可能(デフォルト)。private: クラス内からのみアクセス可能。protected: クラス内、およびそのクラスを継承した子クラス内からのみアクセス可能。
class BankAccount {
public accountNumber: string; // どこからでもアクセス可能
private _balance: number; // クラス内からのみアクセス可能
protected ownerName: string; // クラスと子クラスからアクセス可能
constructor(accountNumber: string, initialBalance: number, ownerName: string) {
this.accountNumber = accountNumber;
this._balance = initialBalance;
this.ownerName = ownerName;
}
public deposit(amount: number): void {
if (amount > 0) {
this._balance += amount;
console.log(`入金しました。新しい残高: ${this._balance}`);
}
}
public withdraw(amount: number): void {
if (amount > 0 && this._balance >= amount) {
this._balance -= amount;
console.log(`出金しました。新しい残高: ${this._balance}`);
} else {
console.log("残高不足、または無効な金額です。");
}
}
// privateなプロパティに外部からアクセスするためのpublicなメソッド(ゲッター)
public getBalance(): number {
return this._balance;
}
protected getOwnerName(): string {
return this.ownerName;
}
}
const myAccount = new BankAccount("12345-678", 10000, "山田太郎");
console.log(`口座番号: ${myAccount.accountNumber}`);
myAccount.deposit(5000);
myAccount.withdraw(2000);
console.log(`現在の残高: ${myAccount.getBalance()}`);
// myAccount._balance; // エラー: '_balance' プロパティは private です。
// console.log(myAccount.ownerName); // エラー: 'ownerName' プロパティは protected です。
_balanceにprivateを付けることで、BankAccountクラスの外から直接残高を操作されることを防ぎ、depositやwithdrawといった公開されたメソッドを通じてのみ残高を変更できるようにしています。これは、データの一貫性を保つための重要な手法です。
デザイナークラスは、オブジェクトを作るための設計図なんですね!プロパティがデータ、メソッドが機能、と考えると分かりやすいです。
エンジニアprivateを使うと、クラスの内部からしか変更できないようにして、データの整合性を守れるのがポイントです。
2. 継承
継承は、既存のクラス(親クラス、基底クラス)のプロパティやメソッドを新しいクラス(子クラス、派生クラス)が引き継ぎ、さらに独自の機能を追加・変更できる仕組みです。これにより、コードの重複を減らし、再利用性を高めることができます。
TypeScriptではextendsキーワードを使って継承を表現します。
class Vehicle { // 親クラス
brand: string;
constructor(brand: string) {
this.brand = brand;
}
start(): void {
console.log(`${this.brand}がエンジンを始動しました。`);
}
stop(): void {
console.log(`${this.brand}がエンジンを停止しました。`);
}
}
class Motorcycle extends Vehicle { // 子クラス
type: string;
constructor(brand: string, type: string) {
super(brand); // 親クラスのコンストラクタを呼び出す
this.type = type;
}
wheelie(): void {
console.log(`${this.brand}の${this.type}がウィリーしています!`);
}
// 親クラスのメソッドをオーバーライド(上書き)
start(): void {
console.log(`${this.brand}の${this.type}がキーを回してエンジンを始動しました。`);
}
}
class Truck extends Vehicle { // 別のクラス
cargoCapacity: number; // 積載量
constructor(brand: string, cargoCapacity: number) {
super(brand);
this.cargoCapacity = cargoCapacity;
}
loadCargo(weight: number): void {
console.log(`${this.brand}トラックに${weight}kgの荷物を積載しました。`);
}
}
const myMotorcycle = new Motorcycle("ハーレーダビッドソン", "クルーザー");
myMotorcycle.start(); // オーバーライドされた子クラスのstart()が呼ばれる
myMotorcycle.wheelie();
myMotorcycle.stop();
const myTruck = new Truck("いすゞ", 5000);
myTruck.start(); // 親クラスのstart()が呼ばれる
myTruck.loadCargo(1000);
myTruck.stop();
MotorcycleとTruckはどちらもVehicleクラスをextendsしています。これにより、brandプロパティやstart(),stop()メソッドを自動的に引き継ぎます。- 子クラスのコンストラクタ内で
super(brand)を呼び出すことで、親クラスのコンストラクタを実行し、親クラスのプロパティを初期化します。super()は子クラスのコンストラクタの先頭で必ず呼び出す必要があります。 Motorcycleクラスでは、親クラスのstart()メソッドをstart(): void { ... }として再定義しています。これを「メソッドのオーバーライド」と呼び、子クラスで親クラスの振る舞いを変更できます。
デザイナー継承のおかげで、似たようなクラスを作るときに、共通部分を何回も書かなくて済むんですね!効率的!
3. インターフェースの実装
インターフェースは、オブジェクトが持つべき「プロパティ」や「メソッド」の型定義(契約)を記述するためのものです。クラスがインターフェースをimplements(実装)することで、そのクラスがインターフェースで定義されたすべてのプロパティとメソッドを持っていることを保証します。
インターフェースは具体的な実装を持ちません。あくまで「このような形をしているべきだ」という取り決めを定義するものです。
// インターフェースの定義
interface Resizable {
resize(percentage: number): void;
}
interface Draggable {
drag(x: number, y: number): void;
}
// Resizableインターフェースを実装するクラス
class Rectangle implements Resizable {
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
resize(percentage: number): void {
this.width *= (1 + percentage / 100);
this.height *= (1 + percentage / 100);
console.log(`Rectangleを${percentage}%リサイズしました。新しいサイズ: ${this.width}x${this.height}`);
}
getArea(): number {
return this.width * this.height;
}
}
// 複数のインターフェースを実装するクラス
class ImageEditorElement implements Resizable, Draggable {
x: number;
y: number;
scale: number;
constructor(x: number, y: number, scale: number) {
this.x = x;
this.y = y;
this.scale = scale;
}
resize(percentage: number): void {
this.scale *= (1 + percentage / 100);
console.log(`要素のスケールを${percentage}%変更しました。新しいスケール: ${this.scale}`);
}
drag(x: number, y: number): void {
this.x += x;
this.y += y;
console.log(`要素を(${x}, ${y})だけ移動しました。新しい位置: (${this.x}, ${this.y})`);
}
// このクラス独自のメソッド
render(): void {
console.log(`要素を位置(${this.x},${this.y})、スケール${this.scale}で描画中...`);
}
}
const myRectangle = new Rectangle(100, 50);
myRectangle.resize(20);
console.log(`長方形の面積: ${myRectangle.getArea()}`);
const myImageElement = new ImageEditorElement(10, 20, 1.0);
myImageElement.render();
myImageElement.drag(5, 5);
myImageElement.resize(10);
myImageElement.render();
ResizableとDraggableという二つのインターフェースを定義しました。これらは、それぞれresizeメソッドとdragメソッドを持つべきであることを示しています。RectangleクラスはResizableをimplementsしているため、resizeメソッドを必ず実装しなければなりません。実装を忘れるとTypeScriptがエラーを報告してくれます。ImageEditorElementクラスはResizableとDraggableの両方を実装しており、それぞれのメソッドを定義しています。これにより、ImageEditorElementはリサイズもドラッグもできるオブジェクトであることが保証されます。
インターフェースは、異なるクラス間で共通の振る舞いを強制したり、コードの柔軟性を高めたりするのに非常に役立ちます。例えば、processResizableObject(obj: Resizable)のような関数を定義すれば、RectangleやImageEditorElementなど、Resizableを実装しているどんなオブジェクトでもこの関数に渡せるようになります。
エンジニアインターフェースは、『このクラスはこんな機能を持ってるよ』というお約束。特にチーム開発では、役割分担を明確にするのに役立ちますよ。
まとめ
今回は、TypeScriptにおけるオブジェクト指向プログラミングの基本を学びました。
- クラスはオブジェクトの「設計図」であり、データ(プロパティ)と機能(メソッド)をカプセル化します。アクセス修飾子を使って、内部のデータへのアクセスを制御できることを理解しました。
- 継承は
extendsキーワードを使い、既存のクラス(親クラス)の機能を再利用し、新しいクラス(子クラス)を作成する仕組みです。super()を使って親クラスのコンストラクタを呼び出すことや、メソッドのオーバーライドについて学びました。 - インターフェースは
implementsキーワードでクラスに実装され、オブジェクトが持つべきプロパティやメソッドの「契約」を定義します。これにより、コードの柔軟性と堅牢性が向上します。
これらのオブジェクト指向の概念は、大規模なアプリケーションを設計・開発する上で非常に強力なツールとなります。コードの整理、再利用、そしてチームでの開発において、これらの知識が大いに役立つでしょう。
デザイナークラス、継承、インターフェース…どれもオブジェクト指向の重要な要素なんですね!複雑なコードも、これで整理しやすくなりそうです。
次回は、TypeScriptの強力な機能の一つである「ジェネリクス」について深く掘り下げていきます。ジェネリクスを理解することで、より汎用的で型安全なコードを書けるようになります。