TypeScript | 関数・クラス・ジェネリクス:クラス設計 – 多重interface実装

TypeScript TypeScript
スポンサーリンク

ゴール:「1つのクラスに“複数の役割”を約束させる感覚をつかむ」

多重 interface 実装は一言でいうと、

「このクラスは A という顔も B という顔も、両方ちゃんと持ちますよ」

と約束させる仕組みです。

implements の右側に interface をカンマ区切りで並べるだけですが、
「役割を分けて考える」という設計の感覚が身につくと、一気に使いどころが見えてきます。


多重interface実装の基本構文

まずは単一 interface 実装をおさらい

単体の interface を implements する形はこうでした。

interface HasId {
  id: number;
}

interface CanSave {
  save(): void;
}

class User implements HasId, CanSave {
  constructor(
    public id: number,
    public name: string
  ) {}

  save(): void {
    console.log(`ユーザー ${this.name} を保存しました`);
  }
}
TypeScript

ここで class User implements HasId, CanSave と書くことで、

  • id: number を持っていること
  • save(): void を持っていること

この両方を「必ず守ります」と宣言しています。

ポイントは、
「1つのクラスが“複数の interface の契約”を同時に守る」
という状態を作れることです。

カンマ区切りでいくつでも並べられる

構文としてはとてもシンプルで、
implements A, B, C のようにカンマで並べるだけです。

interface HasId {
  id: number;
}

interface HasName {
  name: string;
}

interface CanGreet {
  greet(): void;
}

class Person implements HasId, HasName, CanGreet {
  constructor(
    public id: number,
    public name: string
  ) {}

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

この Person は、

  • HasId としても
  • HasName としても
  • CanGreet としても

扱えるクラスになっています。


「多重interface実装」が何をうれしくするのか

クラスに「複数の役割」をはっきりラベル付けできる

例えば、あるクラスが「ID を持つ」「表示できる」「保存できる」という
3つの役割を持っているとします。

interface HasId {
  id: number;
}

interface Displayable {
  display(): void;
}

interface Savable {
  save(): void;
}

class Product implements HasId, Displayable, Savable {
  constructor(
    public id: number,
    public name: string,
    public price: number
  ) {}

  display(): void {
    console.log(`${this.name} (${this.price}円)`);
  }

  save(): void {
    console.log(`商品 ${this.name} を保存しました`);
  }
}
TypeScript

ここでの設計のポイントは、

  • Product というクラスに「HasId」「Displayable」「Savable」という
    3つの“役割ラベル”が付いている
  • それぞれの interface は「その役割に必要なメンバー」だけを定義している
  • クラスはそれらを全部まとめて実装している

ということです。

「このクラスは何ができるのか?」を、
interface の一覧として明示できる

のが、多重 interface 実装の気持ちよさです。

呼び出し側は「必要な顔だけ」を見ることができる

同じ Product でも、
使う側によって「見たい顔」が違います。

function printId(target: HasId) {
  console.log("ID:", target.id);
}

function show(target: Displayable) {
  target.display();
}

function persist(target: Savable) {
  target.save();
}

const p = new Product(1, "ノートPC", 150000);

printId(p);   // HasId として使う
show(p);      // Displayable として使う
persist(p);   // Savable として使う
TypeScript

ここでのポイントは、

  • Product は 3つの interface を全部実装している
  • でも、関数側は「自分が必要な役割だけ」を引数の型として要求している

ということです。

「クラスは多機能でも、呼び出し側は必要な機能だけを前提にできる」
これが、多重 interface 実装の大きなメリットです。


型の観点から見る多重interface実装

「A でもあり B でもある」という型になる

implements A, B と書いたクラスは、
型として「A でもあり B でもあるもの」として扱えます。

interface A {
  a(): void;
}

interface B {
  b(): void;
}

class X implements A, B {
  a(): void {
    console.log("A の機能");
  }

  b(): void {
    console.log("B の機能");
  }
}

const x = new X();

const asA: A = x;
const asB: B = x;

asA.a(); // OK
// asA.b(); // A 型としては b は見えない

asB.b(); // OK
TypeScript

同じ x でも、

  • A 型として見ると「a() だけ持っているもの」
  • B 型として見ると「b() だけ持っているもの」

として扱えます。

「1つの実体に、複数の“見え方(型)”を与える」
というのが、多重 interface 実装の本質です。

実装側は「全部の契約を守る」必要がある

もちろん、クラス側は
実装をサボることはできません。

interface A {
  a(): void;
}

interface B {
  b(): void;
}

class Bad implements A, B {
  a(): void {
    console.log("A だけ実装");
  }

  // b() を実装していないのでコンパイルエラー
}
TypeScript

多重 interface 実装は、

「このクラスは、これらすべての契約を守ります」と
自分でハードルを上げる行為

でもあります。

だからこそ、「このクラスは本当にその役割を全部持つべきか?」を
意識して設計することが大事です。


多重interface実装と継承の組み合わせ

クラス継承+複数 interface 実装もできる

extendsimplements は同時に使えます。

interface CanFly {
  fly(): void;
}

interface CanSwim {
  swim(): void;
}

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

class Duck extends Animal implements CanFly, CanSwim {
  fly(): void {
    console.log(`${this.name} が飛んだ`);
  }

  swim(): void {
    console.log(`${this.name} が泳いだ`);
  }
}
TypeScript

ここでは、

  • 継承(extends Animal):共通の実装・プロパティ(name)を引き継ぐ
  • 多重 interface 実装(implements CanFly, CanSwim):
    「飛べる」「泳げる」という2つの役割を約束する

という役割分担になっています。

「共通の中身は継承で共有し、
“できることのラベル”は interface で付ける」

という設計は、かなり実務でよく使われます。


多重interface実装を設計するときの考え方

「役割ごとに interface を分ける」意識を持つ

多重 interface 実装をうまく使うには、
interface を「役割ごとに小さく分ける」意識が大事です。

例えば、こんな巨大 interface は扱いづらいです。

interface Monster {
  id: number;
  name: string;
  hp: number;
  attack(): void;
  defend(): void;
  talk(): void;
  // まだまだ増える…
}
TypeScript

これを役割ごとに分けてみます。

interface HasStatus {
  hp: number;
}

interface CanAttack {
  attack(): void;
}

interface CanDefend {
  defend(): void;
}

interface CanTalk {
  talk(): void;
}
TypeScript

そして、クラスごとに必要なものだけ implements します。

class Slime implements HasStatus, CanAttack {
  hp = 10;

  attack(): void {
    console.log("スライムの攻撃!");
  }
}

class NPC implements CanTalk {
  talk(): void {
    console.log("こんにちは、旅人さん");
  }
}
TypeScript

こうすると、

  • 「このクラスは何ができるのか」が implements の一覧で一目で分かる
  • 関数側も「必要な役割だけ」を引数の型として要求できる

という、きれいな設計になります。


まとめ:多重interface実装を自分の言葉で整理すると

最後に、あなた自身の言葉でこうまとめてみてください。

多重 interface 実装は、

  • implements A, B, C のように書いて、
    1つのクラスに複数の「契約(役割)」を守らせる仕組み
  • クラスは、そのすべての interface のメンバーを正しい型で実装しなければならない
  • 呼び出し側は「必要な interface 型だけ」を前提にコードを書ける
  • その結果、同じクラスを「いろいろな顔(型)」で扱えるようになる

今あなたのコードの中で、

「1つのクラスがいろんなことをしているのに、
それを1つの型としてしか扱っていない場所」

があれば、

  • その“役割”を小さな interface に分解して
  • クラスに多重 implements させて
  • 関数側は「必要な interface 型だけ」を受け取る

という形にできないか、1カ所だけでいいので考えてみてください。

そこから、
“なんでも屋クラス”が、“役割ごとにラベル付けされたクラス”
少しずつ変わっていきます。

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