ゴール:「この処理、本当にクラスにすべき?」を自分で判断できるようになる
実務で一番大事なのは、
「クラスの書き方」よりも、
「そもそも、ここはクラスにすべきか?」
を判断できることです。
TypeScript は関数・オブジェクト・クラス・ジェネリクス、何でも書けてしまうので、
逆に「なんでもクラス」にすると、すぐにコードが重たくなります。
ここでは、実務で僕が本当に使っている視点に絞って、
クラス設計の判断軸をかみ砕いて話していきます。
まず決めるべきこと:「クラスを使うか、関数で済ませるか」
クラスにする前に必ず自分に聞いてほしいこと
いきなりクラスを設計する前に、
まず自分にこう問いかけてみてください。
「これは“状態を持ったまま、何度も振る舞いを呼び出す”ものか?」
もし答えが「いいえ」なら、
それはたぶんクラスではなく「ただの関数」で十分です。
例えば、こういうのは関数でいいです。
function calcTax(price: number): number {
return Math.floor(price * 0.1);
}
TypeScript状態もないし、
「税計算クラス」をわざわざ作る意味は薄いです。
逆に、こういうのはクラスに向いています。
class ShoppingCart {
private items: { name: string; price: number }[] = [];
addItem(item: { name: string; price: number }) {
this.items.push(item);
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
TypeScriptここでは、
- カートという「状態」を持ち続ける
- その状態に対して「追加」「合計計算」という振る舞いを提供する
という構造になっていて、
クラスにする意味がはっきりあります。
大事なのは、
「状態+振る舞いのセットとして扱いたいか?」
という視点です。
「データの入れ物」だけなら type/interface で十分
もうひとつよくあるのが、
「ただのデータ構造」をクラスにしてしまうパターンです。
例えば、こういうのはクラスにしなくていいことが多いです。
class User {
constructor(
public id: number,
public name: string,
public email: string
) {}
}
TypeScriptこれ、メソッドもなくて、
ただの「ユーザー情報の入れ物」ですよね。
こういう場合は、素直に type や interface で十分です。
interface User {
id: number;
name: string;
email: string;
}
TypeScriptクラスにするのは、
- その型に「意味のある振る舞い」をくっつけたいとき
- 生成のルール(バリデーションなど)を中に閉じ込めたいとき
に絞ると、設計がだいぶスッキリします。
実務でよく出る「クラスにするべき場面」
例1:ドメインの“概念”を表したいとき
EC サイトなら「注文」「カート」「商品」、
チャットアプリなら「メッセージ」「ルーム」「ユーザー」など。
こういう「ビジネス上の概念」は、
クラスにするとコードが読みやすくなります。
class Order {
constructor(
private items: { name: string; price: number }[],
private taxRate: number
) {}
getTotal(): number {
const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
return Math.floor(subtotal * (1 + this.taxRate));
}
}
TypeScriptここでのポイントは、
Orderという名前だけで「何のことか」が伝わる- 合計金額の計算ロジックが、
Orderの中に閉じている - 呼び出し側は「注文に対して total を聞く」という自然な書き方になる
ということです。
実務では、
「ビジネス用語として名前が立っているもの」
は、クラスにすると読みやすくなりやすいです。
例2:ライフサイクルを持つもの(接続・セッションなど)
例えば、API クライアントや WebSocket 接続など。
class ApiClient {
constructor(private baseUrl: string) {}
async get(path: string) {
const res = await fetch(this.baseUrl + path);
return res.json();
}
}
TypeScriptこういう「接続」「セッション」「クライアント」は、
- 設定(baseUrl、トークンなど)を持ち続ける
- その設定を使って何度もメソッドを呼ぶ
という構造なので、クラスに向いています。
関数だけで書くと、
- 毎回 baseUrl を渡す必要がある
- 設定がバラバラに散らばる
といった問題が出やすくなります。
例3:状態をまたいで処理が進むもの(状態機械・ウィザードなど)
例えば、画面のステップが進んでいくウィザード。
class SignupFlow {
private step: number = 1;
next() {
this.step++;
}
get currentStep() {
return this.step;
}
}
TypeScriptこういう「状態が変化していくもの」は、
クラスにまとめると追いやすくなります。
実務では、
- フォームの状態管理
- ステップごとのバリデーション
- 状態に応じたボタンの活性・非活性
などをクラスに閉じ込めると、
UI 側のコードがかなりスッキリします。
実務での判断軸:「クラスを分ける/まとめる」の基準
判断軸1:責務が1つかどうか(1クラス1役割)
クラス設計で一番壊れやすいのがここです。
例えば、こんなクラスは危険信号です。
class UserService {
async fetchUser() { /* API 呼び出し */ }
validateUser() { /* バリデーション */ }
renderUser() { /* DOM 操作して表示 */ }
}
TypeScriptAPI 呼び出し・バリデーション・画面描画。
役割がバラバラに混ざっています。
こういうときは、責務ごとに分けた方がいいです。
class UserApi {
async fetchUser() { /* API 呼び出し */ }
}
class UserValidator {
validateUser() { /* バリデーション */ }
}
class UserView {
renderUser() { /* 表示 */ }
}
TypeScript実務での感覚としては、
「このクラスの説明を一文で言えるか?」
が基準になります。
「ユーザーを取得するクラスです」
「ユーザーを検証するクラスです」
と言えるなら OK。
「ユーザーを取得して検証して画面に出すクラスです」
と言い出したら、もう分けた方がいいです。
判断軸2:テストしやすいかどうか
実務では、テストのしやすさも重要な判断材料です。
例えば、こんなクラス。
class UserService {
async getUserName(id: number): Promise<string> {
const res = await fetch(`/users/${id}`);
const data = await res.json();
return data.name.toUpperCase();
}
}
TypeScriptこのクラスは、
- ネットワーク呼び出し
- データ整形(
toUpperCase)
が混ざっています。
テストしたいのは本当は「整形ロジック」だけなのに、
毎回 API をモックしないといけなくなります。
これを分けると、テストが楽になります。
class UserApi {
async fetchUser(id: number) { /* fetch */ }
}
class UserFormatter {
formatName(name: string): string {
return name.toUpperCase();
}
}
TypeScript実務では、
「このクラスをテストするとき、余計なものをたくさんモックしないといけないなら、責務が多すぎる」
と考えてみてください。
判断軸3:将来の変更に強いかどうか
クラス設計の良し悪しは、
「変更が入ったとき」に露骨に出ます。
例えば、User の表示ルールが変わったときに、
- 1クラスだけ直せば済む
- 3〜4クラスにまたがって修正が必要
どちらになりそうか、を想像してみてください。
実務では、
- 「変わりやすいもの」は分離する
- 「一緒に変わるもの」は同じクラスにまとめる
という感覚が大事です。
API の仕様は変わるかもしれない。
画面のデザインも変わるかもしれない。
でも、ビジネスルール(例えば「合計金額の計算」)は比較的安定している。
そういう「変わり方の違い」を意識してクラスを分けると、
後からの修正コストがかなり変わります。
実務でありがちな「やりすぎ/やらなさすぎ」パターン
やりすぎパターン:なんでもかんでもクラス
初心者がハマりがちなのがこれです。
- ただの変換処理をクラスにする
- 1回しか使わない処理をクラスにする
- 状態もないのにクラスにする
結果として、
- ファイル数だけ増える
- クラス名を考えるのに疲れる
- どこに何があるか分からなくなる
という状態になります。
「状態を持たない」「ビジネス上の意味も薄い」処理は、
まずは関数で書いてみてください。
クラスは、
「名前を与えたい概念」
「状態を持ち続けるもの」
「振る舞いをまとめたいもの」
に絞るとちょうどいいです。
やらなさすぎパターン:全部関数でベタ書き
逆に、全部関数で書いてしまうパターンもあります。
let items: { name: string; price: number }[] = [];
function addItem(item: { name: string; price: number }) {
items.push(item);
}
function getTotal() {
return items.reduce((sum, item) => sum + item.price, 0);
}
TypeScript最初はこれでも動きますが、
- カートが2つ必要になったら?
- 「割引付きカート」と「通常カート」を分けたくなったら?
といったときに、一気に破綻します。
こういうときは、素直にクラスにしてしまった方がいいです。
class ShoppingCart {
private items: { name: string; price: number }[] = [];
addItem(item: { name: string; price: number }) {
this.items.push(item);
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
TypeScript実務では、
「同じ“種類のもの”を複数持ちたいとき」
は、クラスにするサインだと思ってください。
まとめ:実務でのクラス設計判断を自分の言葉で整理すると
最後に、あなた自身の言葉でこうまとめてみてください。
クラスにするのは、
- 状態+振る舞いをセットで扱いたいとき
- ビジネス上の概念(注文・カート・ユーザーなど)を表したいとき
- ライフサイクルやセッションを持つもの(API クライアントなど)のとき
- 複数個並べて扱いたい「同じ種類のもの」があるとき
クラスを分けるかどうかは、
- 「このクラスの説明を一文で言えるか?」
- 「テストするときに余計なモックが増えすぎないか?」
- 「将来の変更で一緒に変わるもの同士をまとめているか?」
で判断する。
今書いているコードの中から、
1つだけ「これはクラスにした方がよさそう」「逆にクラスやめて関数でよさそう」
と思う場所を選んで、実際に書き換えてみてください。
その小さな一歩を何度も繰り返すうちに、
「クラス設計の判断」は、ちゃんと自分の感覚として育っていきます。
