クラスとインターフェース
TypeScriptにおけるクラスとインターフェースの定義方法とベストプラクティスを学びましょう。
学習のポイント
- クラスの情報は出来る限り隠蔽する
- 使用側がクラス内部について知らなくても使えるようにする
- クラスはなるべく小さく作る
- クラス継承とコンポジションの違いを知る
- 凝集度と結合度を意識する
クラスはなるべく小さく作る
大きすぎるクラスは単一責任の原則に違反している可能性があります。適切に分割しましょう。
Bad
❌ Bad: 複数の責任を持つクラス
// ❌ 複数の責任を持つクラス
class Cart {
private items: CartItem[] = [];
addProduct(product: Product): void { /* ... */ }
getProductPrice(productId: string): number { /* ... */ }
getProductCoupon(productId: string): Coupon | null { /* ... */ }
calculate(): number { /* ... */ }
printReceipt(): void { /* ... */ }
}
Good
✅ Good: 責任を分離
// ✅ 責任を分離
class Product {
constructor(
private readonly id: string,
private readonly price: number,
private readonly coupon: Coupon | null = null
) {}
getPrice(): number {
return this.price;
}
getCoupon(): Coupon | null {
return this.coupon;
}
}
class Cart {
private items: Product[] = [];
constructor(initialItems: Product[] = []) {
this.items = initialItems;
}
add(product: Product): void {
this.items.push(product);
}
calculate(): number {
return this.items.reduce((total, item) => total + item.getPrice(), 0);
}
}
class ReceiptPrinter {
print(cart: Cart): void {
// レシート印刷処理
}
}
クラス内部の情報は出来る限り隠蔽する
TypeScriptでは private, protected, readonly 修飾子を使って情報を隠蔽できます。
Bad
❌ Bad: 内部状態が公開されている
// ❌ 内部状態が公開されている
class Television {
modelNumber: string = '型番';
displayModelNumber(): void {
console.log(this.modelNumber);
}
}
const tv = new Television();
tv.modelNumber = 'バグを引き起こす値'; // 外部から変更可能
Good
✅ Good: private と readonly で保護
// ✅ private と readonly で保護
class Television {
private readonly modelNumber: string = '型番';
displayModelNumber(): void {
console.log(this.modelNumber);
}
getModelNumber(): string {
return this.modelNumber;
}
}
const tv = new Television();
// tv.modelNumber = '変更'; // コンパイルエラー
tv.displayModelNumber(); // 正しい使い方
プライベートフィールド(#)
TypeScriptでは JavaScript のプライベートフィールド(#)も使用可能です。
class Circle {
// # で始まるフィールドはランタイムでもプライベート
#radius: number;
readonly #PI = Math.PI;
constructor(radius: number) {
this.#radius = radius;
}
getArea(): number {
return this.#PI * this.#radius ** 2;
}
getPerimeter(): number {
return 2 * this.#PI * this.#radius;
}
// getter/setter でアクセスを制御
get radius(): number {
return this.#radius;
}
set radius(newRadius: number) {
if (newRadius <= 0) {
throw new Error('Radius must be positive');
}
this.#radius = newRadius;
}
}
使用側がクラス内部を知らなくても使えるようにする
クラスの内部構造を知らなくても使えるようにすることで、使いやすいAPIを提供できます。
Bad
❌ Bad: 内部構造を知る必要がある
// ❌ 内部構造を知る必要がある
class Television {
power: boolean = true;
switch: Switch = new Switch();
}
class Switch {
toggle(power: boolean): boolean {
return !power;
}
}
const tv = new Television();
// 使用者がSwitch.toggleの存在を知る必要がある
tv.power = tv.switch.toggle(tv.power);
Good
✅ Good: シンプルなAPIを提供
// ✅ シンプルなAPIを提供
class Television {
private power: boolean = false;
private readonly powerSwitch: Switch = new Switch();
togglePower(): void {
this.power = this.powerSwitch.toggle(this.power);
console.log(`Power: ${this.power ? 'ON' : 'OFF'}`);
}
isPoweredOn(): boolean {
return this.power;
}
}
class Switch {
toggle(state: boolean): boolean {
return !state;
}
}
const tv = new Television();
tv.togglePower(); // 内部実装を知 らなくても使える
クラス継承とコンポジション
継承(is-a 関係)
「AはBである」という関係のときに使用します。
// Animal is a Animal の関係
abstract class Animal {
abstract makeSound(): void;
eat(): void {
console.log('食べる');
}
sleep(): void {
console.log('寝る');
}
}
class Dog extends Animal {
makeSound(): void {
console.log('ワン!');
}
sleep(): void {
console.log('小屋で寝る'); // オーバーライド
}
}
const dog = new Dog();
dog.eat(); // 食べる(継承)
dog.sleep(); // 小屋で寝る(オーバーライド)
dog.makeSound(); // ワン!
コンポジション(has-a 関係)
「AはBを持つ」という関係のときに使用します。
// Television has a Switch の関係
class Switch {
toggle(state: boolean): boolean {
return !state;
}
}
class Television {
private power: boolean = false;
private readonly powerSwitch: Switch; // コンポジション
constructor() {
this.powerSwitch = new Switch();
}
togglePower(): void {
this.power = this.powerSwitch.toggle(this.power);
}
}
継承よりコンポジションを優先する
継承は強い結合を生むため、多くの場合コンポジションの方が柔軟です。
❌ Bad: 継承:Attack is a Hero/Enemy という関係は不自然
// ❌ 継承:Attack is a Hero/Enemy という関係は不自然
class Attack {
punch(target: string, damage: number): void {
console.log(`${target}に${damage}ダメージを与えた`);
}
}
class Hero extends Attack { }
class Enemy extends Attack { }
// ✅ コンポジション:Hero/Enemy has an Attack
interface AttackAction {
execute(target: string): void;
}
class PunchAttack implements AttackAction {
constructor(private damage: number) {}
execute(target: string): void {
console.log(`${target}に${this.damage}ダメージを与えた`);
}
}
class Hero {
constructor(private attack: AttackAction) {}
performAttack(target: string): void {
this.attack.execute(target);
}
}
// 攻撃方法を柔軟に変更可能
const hero = new Hero(new PunchAttack(10));
hero.performAttack('スライム');
インターフェースを活用する
TypeScriptのインターフェースは、クラスの契約を定義するのに最適です。
// インターフェースで契約を定義
interface PaymentMethod {
processPayment(amount: number): Promise<PaymentResult>;
refund(transactionId: string): Promise<RefundResult>;
}
// 複数の実装を提供
class CreditCardPayment implements PaymentMethod {
async processPayment(amount: number): Promise<PaymentResult> {
// クレジットカード決済処理
return { success: true, transactionId: 'cc_123' };
}
async refund(transactionId: string): Promise<RefundResult> {
// 返金処理
return { success: true };
}
}
class PayPalPayment implements PaymentMethod {
async processPayment(amount: number): Promise<PaymentResult> {
// PayPal決済処理
return { success: true, transactionId: 'pp_456' };
}
async refund(transactionId: string): Promise<RefundResult> {
// 返金処理
return { success: true };
}
}
// 使用側はインターフェースに依存
class PaymentProcessor {
constructor(private paymentMethod: PaymentMethod) {}
async checkout(amount: number): Promise<PaymentResult> {
return this.paymentMethod.processPayment(amount);
}
}
凝集度を高める
凝集度とは、モジュール内のデータとロジックの関係性の強さを表す指標です。
Bad(低凝集)
❌ Bad: データとロジックが分散
// ❌ データとロジックが分散
class Product {
price: number = 0;
}
class Shop {
getTaxFreePrice(product: Product): number {
return product.price;
}
getTaxIncludedPrice(product: Product): number {
return product.price * 1.1;
}
}
class OnlineShop {
// 同じロジックを再実装...
getTaxFreePrice(product: Product): number {
return product.price;
}
getTaxIncludedPrice(product: Product): number {
return product.price * 1.1;
}
}
Good(高凝集)
✅ Good: データとロジックを一箇所にまとめる
// ✅ データとロジックを一箇所にまとめる
class Product {
private readonly TAX_RATE = 0.1;
constructor(private readonly price: number) {}
getTaxFreePrice(): number {
return this.price;
}
getTaxIncludedPrice(): number {
return this.price * (1 + this.TAX_RATE);
}
}
// Shop と OnlineShop は Product のメソッドを使うだけ
class Shop {
checkout(products: Product[]): number {
return products.reduce((total, p) => total + p.getTaxIncludedPrice(), 0);
}
}
結合度を低くする(疎結合)
結合度とは、モジュール間の依存度合いを表す指標です。