TypeScript | 関数・クラス・ジェネリクス:クラス設計 – クラスの再利用設計

TypeScript TypeScript
スポンサーリンク

ゴール:「このクラス、別の場面でもそのまま使えるな」と思える設計感覚を身につける

クラスの再利用設計は、一言でいうと、

「今この機能だけ動けばいい」ではなく、「あとで別の場所でも気持ちよく使える形」にしておくこと

です。

ここを意識し始めると、
「とりあえず動くコード」から
「長く付き合えるコード」に一段レベルアップします。

ここでは、TypeScript のクラスを題材にしながら、
初心者でも実感しやすい「再利用しやすいクラスの作り方」を、例を通してかみ砕いていきます。


再利用しづらいクラスの典型例からスタートする

「今この画面でしか使えない」クラス

まずは、よくある“もったいない”クラスから。

class UserFormController {
  private users: { id: number; name: string }[] = [];

  addUserFromInput(inputId: string, inputName: string): void {
    const idElement = document.getElementById(inputId) as HTMLInputElement;
    const nameElement = document.getElementById(inputName) as HTMLInputElement;

    const id = Number(idElement.value);
    const name = nameElement.value;

    this.users.push({ id, name });

    alert(`ユーザー ${name} を追加しました`);
  }

  getUsers(): { id: number; name: string }[] {
    return this.users;
  }
}
TypeScript

このクラスは、一見便利そうに見えますが、
再利用という観点ではかなり厳しいです。

理由を言葉にすると、こうなります。

ブラウザの document にベッタリ依存していて、
「フォームから値を取ってきて、alert で出す」という
“特定の画面の事情”がクラスの中にべったり埋め込まれているからです。

このクラスを、
「CLI ツールでも使いたい」
「React コンポーネントからも使いたい」
と思っても、ほぼそのままでは使えません。

再利用しづらいクラスの特徴を整理する

再利用しづらいクラスは、だいたいこんな状態になっています。

一つのクラスの中に、

ビジネスロジック(ユーザーを追加する)と
UI の都合(DOM を触る、alert を出す)が
ごちゃっと混ざっている。

つまり、

「このクラスは“何をするクラスなのか”がはっきりしていない」

ということです。

再利用設計の第一歩は、
ここをきれいに分けるところから始まります。


再利用しやすいクラスの基本:役割を「純粋」にする

「純粋なロジック」と「環境依存の部分」を分ける

先ほどの例を、「再利用しやすい形」に分解してみます。

まず、「ユーザーを管理する」という“純粋なロジック”だけを取り出します。

type User = { id: number; name: string };

class UserStore {
  private users: User[] = [];

  add(user: User): void {
    this.users.push(user);
  }

  getAll(): User[] {
    return this.users;
  }
}
TypeScript

この UserStore は、
ブラウザにも、フォームにも、alert にも依存していません。

やっていることはただ一つ。

「ユーザーを追加して、一覧を返す」

だけです。

この時点で、
CLI でも、Web でも、テストコードでも、
どこからでも同じように使える“再利用可能なクラス”になっています。

UI 側は「UserStore をどう使うか」だけに集中させる

次に、ブラウザの事情は別のクラスに閉じ込めます。

class UserFormController {
  constructor(private store: UserStore) {}

  addUserFromInput(inputId: string, inputName: string): void {
    const idElement = document.getElementById(inputId) as HTMLInputElement;
    const nameElement = document.getElementById(inputName) as HTMLInputElement;

    const id = Number(idElement.value);
    const name = nameElement.value;

    this.store.add({ id, name });

    alert(`ユーザー ${name} を追加しました`);
  }

  getUsers(): User[] {
    return this.store.getAll();
  }
}
TypeScript

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

UserStore は「ユーザー管理」という再利用しやすいロジックだけを持ち、
UserFormController は「ブラウザのフォームと UserStore をつなぐ役」に専念していることです。

もし将来、
「フォームではなく API からユーザーを追加したい」
となったら、UserStore はそのまま使い回せます。

再利用しやすいクラスは、
「環境に依存しない“純粋なロジック”だけを持っている」
ことが多いです。


コンストラクタ注入で「差し替え可能」にしておく

依存する相手を「自分で new しない」

再利用設計でとても大事なのが、

「クラスの中で、他のクラスを直接 new しない」

という感覚です。

例えば、こう書いてしまうと再利用性が下がります。

class UserService {
  private store = new UserStore();

  createUser(id: number, name: string): void {
    this.store.add({ id, name });
  }
}
TypeScript

この書き方だと、
UserService は「UserStore という具体クラス」にベッタリ依存しています。

もし将来、

「UserStore の代わりに、DB に保存する UserRepository を使いたい」

となったとき、UserService の中身を書き換える必要が出てきます。

再利用しやすい形にするなら、こうします。

class UserService {
  constructor(private store: UserStore) {}

  createUser(id: number, name: string): void {
    this.store.add({ id, name });
  }
}
TypeScript

UserService は「UserStore を受け取る側」に回り、
「どの実装を渡すか」は外側が決めるようにします。

これが「コンストラクタ注入」の基本的な考え方です。

interface と組み合わせると、さらに柔らかくなる

UserStore の役割を interface で表現すると、
差し替えがもっと簡単になります。

interface IUserStore {
  add(user: User): void;
  getAll(): User[];
}

class MemoryUserStore implements IUserStore {
  private users: User[] = [];

  add(user: User): void {
    this.users.push(user);
  }

  getAll(): User[] {
    return this.users;
  }
}
TypeScript

UserService は「IUserStore という役割」にだけ依存します。

class UserService {
  constructor(private store: IUserStore) {}

  createUser(id: number, name: string): void {
    this.store.add({ id, name });
  }
}
TypeScript

こうしておけば、

テストでは「メモリ上のダミー実装」、
本番では「DB に保存する実装」など、
好きなものを差し替えられます。

再利用しやすいクラスは、

「自分で依存先を決めず、“こういう役割のものをください”とだけ言う」

ように設計されていることが多いです。


汎用クラスに寄せる:具体的な型に縛られすぎない

まずは「User 専用」から始めていい

最初から完璧な汎用クラスを作ろうとすると、
だいたい失敗します。

なので、最初は「User 専用」で構いません。

class UserStore {
  private users: User[] = [];

  add(user: User): void {
    this.users.push(user);
  }

  getAll(): User[] {
    return this.users;
  }
}
TypeScript

これを使っているうちに、
「これ、他の型でも同じようなことやってるな」と気づいたら、
そこで初めて汎用化を考えれば十分です。

同じパターンが増えてきたら、ジェネリクスで抽象化する

例えば、Product でも同じような Store を作り始めたとします。

type Product = { id: number; name: string };

class ProductStore {
  private products: Product[] = [];

  add(product: Product): void {
    this.products.push(product);
  }

  getAll(): Product[] {
    return this.products;
  }
}
TypeScript

ここで、「UserStore とほぼ同じだな」と感じたら、
ジェネリクスで汎用化できます。

class Store<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  getAll(): T[] {
    return this.items;
  }
}

const userStore = new Store<User>();
userStore.add({ id: 1, name: "Taro" });

const productStore = new Store<Product>();
productStore.add({ id: 1, name: "ノートPC" });
TypeScript

こうすると、

「配列に追加して、全部返す」

というロジックを一度だけ書いて、
いろんな型で再利用できます。

再利用設計のコツは、

最初からジェネリクスを振り回すのではなく、
「同じパターンが2回、3回と出てきたら抽象化する」
くらいのタイミング感で十分です。


「再利用しやすいか」をチェックするシンプルな視点

そのクラスを「別プロジェクトでも使いたい」と思えるか?

今書いているクラスを見て、
こう自問してみてください。

「このクラス、別のプロジェクトでもそのまま持っていって使えそう?」

もし、

画面の事情にベッタリだったり、
特定のライブラリに強く依存していたり、
ビジネスルールと UI が混ざっていたりするなら、
再利用性は低いです。

逆に、

「これは“ユーザーを管理するだけ”のクラスだな」
「これは“ログを出すだけ”のクラスだな」

と説明できるなら、
それはかなり再利用しやすいクラスになっています。

テストコードから「単体で」呼べるか?

もう一つの視点は、

「このクラスを、テストコードから単体で呼べるか?」

です。

例えば、UserStore のようなクラスは、
テストからこう呼べます。

const store = new UserStore();
store.add({ id: 1, name: "Taro" });
store.add({ id: 2, name: "Hanako" });

console.log(store.getAll());
TypeScript

ブラウザも、DB も、外部サービスもいりません。

テストしやすいクラスは、
だいたい再利用もしやすいです。

なぜなら、

「外部環境に依存せず、純粋なロジックだけを持っている」

からです。


まとめ:クラスの再利用設計を自分の言葉で整理すると

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

クラスの再利用設計とは、

「今だけ動けばいいクラス」ではなく、
「別の場面・別のプロジェクトでも、そのまま使い回せるクラス」を意識して設計すること。

そのために大事なのは、

クラスの責務を絞ること。
環境依存の部分(UI・ブラウザ・DB など)と、
純粋なロジックを分けること。
依存先はコンストラクタで受け取り、
自分で new しないこと。
同じパターンが増えてきたら、ジェネリクスや interface で抽象化すること。

今のあなたのコードの中から、
「これは他の場所でも使えそうだな」というクラスを1つだけ選んでみてください。

そのクラスから、

環境依存の処理を外に追い出せないか?
依存している相手をコンストラクタで受け取る形にできないか?
ジェネリクスにできそうなパターンはないか?

を一つずつ見直してみると、
「再利用しやすいクラス設計」の感覚が、かなりリアルに掴めてきます。

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