ゴール:「このクラス、別の場面でもそのまま使えるな」と思える設計感覚を身につける
クラスの再利用設計は、一言でいうと、
「今この機能だけ動けばいい」ではなく、「あとで別の場所でも気持ちよく使える形」にしておくこと
です。
ここを意識し始めると、
「とりあえず動くコード」から
「長く付き合えるコード」に一段レベルアップします。
ここでは、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 });
}
}
TypeScriptUserService は「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;
}
}
TypeScriptUserService は「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つだけ選んでみてください。
そのクラスから、
環境依存の処理を外に追い出せないか?
依存している相手をコンストラクタで受け取る形にできないか?
ジェネリクスにできそうなパターンはないか?
を一つずつ見直してみると、
「再利用しやすいクラス設計」の感覚が、かなりリアルに掴めてきます。
