【無料配布】10日間で学べるTypeScript学習資料

プログラミングのクラスとは?設計の基礎から継承・実務での活用法まで解説

プログラミング中級者の壁になりがちな「クラス」の概念について、設計図と実体(インスタンス)の違いからTypeScriptでの実践的な書き方までを網羅しました。

初心者によくある落とし穴や、実務で役立つクラス設計のベストプラクティスまで学べる完全ガイドです。

目次

プログラミングのクラスとは?

プログラミングの学習を進めていくと、中級者への登竜門として必ず登場するのが「クラス」という言葉です。

ネットで「プログラミング クラスとは」や「プログラミング クラス概念」と検索して、難しそうな説明に圧倒されてしまった方も多いのではないでしょうか。

プログラミングにおけるクラスとは、一言で言えば「データと処理をひとまとめにするための仕組み」です。

多くのプログラミング言語はクラスの仕組みを持っており、現代のシステム開発を支える重要なプログラミング用語がクラスです。

ここでは、クラスの意味や本質、初心者向けにプログラミングのクラスをTypeScriptのコードを交えて解説します。

プログラミングのクラスとは?
  • クラスは「設計図」、インスタンスは「実体」
  • なぜクラスが必要?オブジェクト指向プログラミングのメリット
  • 関数や構造体とクラスはどう違う?

クラスは「設計図」、インスタンスは「実体」

プログラミング クラス を理解する上で、よく使われるのが「設計図」と「実体」の例えです。

  • クラス:製品を作るための「設計図」(枠組みやルールを決めたもの)
  • インスタンス:設計図を元に実際に作られた「実体(プログラミング クラス オブジェクト)」

例えば、「たい焼きの型」がクラスだとすれば、そこから焼き上げられた個々の「あんこ味のたい焼き」や「カスタード味のたい焼き」がインスタンスにあたります。

型(クラス)は1つですが、そこから中身の異なる実体(インスタンス)を何個でも大量生産できるのが、クラスとインスタンスの関係性です。

初心者が実務でよく起こす混乱が、「クラス(設計図)をそのまま動かそうとしてしまう」ことです。

クラスはあくまで設計図にすぎないため、そのままではデータを保存したり処理を実行したりできません。

newキーワードを使って「インスタンス(実体)」化してから操作する必要があります。

実務では、このインスタンス化を忘れてクラス名に対して直接メソッドを呼び出そうとし、コンパイルエラーになるケースが多発します。

// ⭕ クラス(設計図)の定義
class Taiyaki {
  // プロパティ(たい焼きの状態・データ)
  taste: string;

  constructor(taste: string) {
    this.taste = taste; // インスタンス化するときに中身(味)を決める
  }

  // メソッド(たい焼きの振る舞い・処理)
  showTaste(): void {
    console.log(`このたい焼きの中身は ${this.taste} です。`);
  }
}

// ❌ 初心者がやりがちなミス
// Taiyaki.showTaste(); // 🚨 エラー:設計図のままでは呼び出せない!

// ⭕ 正しい使い方:new を使ってインスタンス(実体)を生成する
const taiyakiAnko = new Taiyaki("あんこ");
const taiyakiCream = new Taiyaki("カスタード");

// インスタンス(オブジェクト)に対して処理を呼び出す
taiyakiAnko.showTaste();  // 出力: このたい焼きの中身は あんこ です。
taiyakiCream.showTaste(); // 出力: このたい焼きの中身は カスタード です。

なぜクラスが必要?オブジェクト指向プログラミングのメリット

クラスを活用してプログラムを組み立てていく手法を「オブジェクト指向プログラミング」と呼びます。

では、わざわざクラスという複雑な仕組みを使うメリットは何でしょうか。

最大の利点は、「データと、そのデータを操作する関数を密結合(グループ化)させ、コードの管理を楽にする」点にあります。

もしクラスを使わずに複数のデータを管理しようとすると、データと処理がバラバラになり、ある変数を関係のない別の関数が勝手に書き換えてしまうといったバグが発生しやすくなります。

クラスの中に閉じ込めておくことで、データの安全性が高まり、大規模な開発でも破綻しないコードが書けるようになります。

オブジェクト指向に慣れていない初心者が実務でやりがちなのが、「1つのクラスに何でもかんでも機能を持たせすぎてしまう(巨大すぎるクラスの作成)」ことです。

例えば、Userクラスの中に、ユーザー情報だけでなく、商品の決済処理やメール送信のシステムまで詰め込んでしまうようなケースです。

これは「単一責任の原則」に反し、かえってコードの可読性を下げ、修正時の影響範囲が広がる原因になります。

クラスは「1つの役割・関心事」に絞って小さく設計するのがよいです。

// ⭕ 適切なサイズに設計されたクラスの例(銀行口座の管理)
class BankAccount {
  private balance: number = 0; // 外部から直接書き換えられないよう保護(カプセル化)

  // 預金するメソッド
  deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
    }
  }

  // 残高を確認するメソッド
  getBalance(): number {
    return this.balance;
  }
}

const myAccount = new BankAccount();
myAccount.deposit(5000); // 正当なルート(メソッド)を通してのみデータを変更できる

// ❌ 外部から直接書き換えようとするとエラーになるため安全
// myAccount.balance = -10000; // 🚨 エラー: プロパティ 'balance' はプライベートであり、クラス 'BankAccount' 内でのみアクセス可能です。
console.log(`現在の残高: ${myAccount.getBalance()}円`); // 出力: 現在の残高: 5000円

関数や構造体とクラスはどう違う?

クラスを学んでいると、「それってクラスや関数だけじゃダメなの?」「構造体(オブジェクトの型定義)と何が違うの?」という疑問が湧いてきます。

それぞれのクラスや関数の違いを整理すると以下のようになります。

  • 関数:純粋な「処理(手続き)」のみを表すもの。状態を持たない。
  • 構造体 / オブジェクト(型定義):純粋な「データ(値)」の集まりを表すもの。処理を持たない。
  • クラス:「データ(構造体)」と「処理(関数)」を2つセットでドッキングしたもの。

もしクラスを使わない場合、データ(オブジェクト)を加工するために、毎回そのデータを別の関数に引数として渡す必要があります。

これだと、データ構造が変わった際に関数側もすべて修正しなければならず、開発規模が大きくなると修正漏れなどの限界を迎えます。

TypeScript(Reactなど)のモダン開発では、あえてクラスを使わずに「純粋なオブジェクトの型(typeinterface)」と「独立した関数」を組み合わせる「関数型プログラミング」のアプローチをとることも非常に多いです。

そのため、実務での盲点は、「今作っているプロジェクトの設計方針(オブジェクト指向か関数型か)を無視して、何でもかんでもクラスで書こうとしてしまうこと」です。

チームのコーディング規約やフレームワークの特性に合わせて、クラスを使うべきか、使わないべきかを見極める必要があります。

// 1️⃣ 【クラスを使わない場合】(構造体/型定義 + 関数の組み合わせ)
interface UserType {
  name: string;
  age: number;
}

// 処理は完全に外側の関数として独立している
function celebrateBirthday(user: UserType): UserType {
  return { ...user, age: user.age + 1 }; // 新しいオブジェクトを返す(データと処理がバラバラ)
}


// 2️⃣ 【クラスを使う場合】(データと処理を一体化)
class UserClass {
  constructor(public name: string, public age: number) {}

  // 処理(メソッド)がデータ(プロパティ)に直接紐付いている
  celebrateBirthday(): void {
    this.age += 1; // 自分自身の状態を変更できる
  }
}

// 💡 どちらの書き方にもメリット・デメリットがあります。
// 状態をカプセル化して厳密に管理したい場合は2️⃣(クラス)、
// データを不変(イミュータブル)に保ち、テストを簡単にしたい場合は1️⃣(関数型)が実務で選ばれます。

【TypeScript】クラスの基本的な使い方・書き方

クラスの概念(設計図と実体)が理解できたら、次は「クラスの使い方」の実践です。

TypeScriptでクラスを書くための基本構文は、とてもシンプルです。

「データ」と「処理」を1つのブロック{ }の中にまとめて書くだけで設計図が完成します。

ここでは、TypeScriptにおけるクラスの書き方と実務で必須となる初期化の仕組みを解説します。

クラスの基本的な使い方・書き方
  • クラスの宣言とプロパティ(変数)、メソッド(関数)
  • constructor(コンストラクタ)による初期化処理
  • 実際のコード例で見るクラスの作り方

クラスの宣言とプロパティ(変数)、メソッド(関数)

クラスという箱の中には、主に2つの要素が入ります。

それが「プロパティ」と「メソッド」です。

  • プロパティ
    クラスが持つ「データ・状態」のことです。
    通常の変数(letconst)と同じ役割ですが、クラスの中では「プロパティ(またはメンバ変数)」と呼ばれます。
  • メソッド
    クラスが持つ「処理・振る舞い」のことです。
    「プログラミング クラス メソッド と は何か?」と聞かれたら、「そのクラス専用の関数」と答えて問題ありません。
    通常のfunctionと同じように動きます。

初心者がクラスを書き始めて100%つまずくのが、「クラス内で自分自身のプロパティを使う際にthis.を付け忘れる」というミスです。

通常の関数であれば変数名をそのまま書けば使えますが、クラスのメソッド内で自身のデータにアクセスするには、必ず「私の(this)」というキーワードを付ける必要があります。

これを忘れると「その変数は定義されていません」というエラーになります。

// ⭕ クラスの宣言(名前の頭文字は大文字にするのがルールです)
class Smartphone {
  // 1. プロパティ(変数:データ・状態)
  batteryLevel: number = 100; // 初期値として100を代入
  modelName: string = "iPhone";

  // 2. メソッド(関数:処理・振る舞い)
  charge(): void {
    // ❌ よくあるミス: batteryLevel = 100; とそのまま書いてしまう
    // ⭕ 正しい書き方: this. を付けて「自身のプロパティ」であることを明示する
    this.batteryLevel = 100;
    console.log(`${this.modelName}の充電が完了しました。`);
  }
}

constructor(コンストラクタ)による初期化処理

前項の例ではmodelNameを”iPhone”と固定していましたが、これでは「Galaxy」や「Pixel」など別のスマホ(インスタンス)を作りたい時に困ってしまいます。

そこで登場するのが「constructor(コンストラクタ)」です。

コンストラクタは、newキーワードを使って設計図から実体(インスタンス)を作り出す瞬間に「たった一度だけ自動で実行される特別なメソッド」です。

ここで、インスタンスごとに異なる初期データを流し込みます。

TypeScriptの実務において初心者がよく指摘されるのが、「プロパティの宣言とコンストラクタの代入を二重に書いてしまい、コードが冗長になる(長くなる)」ことです。

TypeScriptには、コンストラクタの引数にpublicprivateといったアクセス修飾子を付けるだけで、「プロパティの宣言」と「初期値の代入」を裏側で自動的にやってくれる「省略記法(パラメータープロパティ)」という強力な機能があります。

実務ではほぼ100%この省略記法が使われるため、絶対に覚えておきましょう。

// ❌ 初心者が書きがちな冗長な書き方(昔のJavaScriptに近い書き方)
class UserOld {
  name: string; // ①プロパティを宣言して
  age: number;

  constructor(name: string, age: number) {
    this.name = name; // ②受け取った引数を代入する(記述が重複している)
    this.age = age;
  }
}

// ⭕ TypeScriptの実務で標準となる「省略記法(スマートな書き方)」
class UserNew {
  // constructorの引数に public などを付けるだけで、プロパティ宣言と代入が自動で行われる!
  constructor(public name: string, public age: number) {
    // 中身は空っぽでOK(裏側で this.name = name が実行されています)
  }

  introduce(): void {
    console.log(`私は${this.name}、${this.age}歳です。`);
  }
}

const user1 = new UserNew("田中", 25);
user1.introduce(); // 出力: 私は田中、25歳です。

実際のコード例で見るクラスの作り方

それでは、これまでの知識を組み合わせて、より実践的な「クラス例」を見てみましょう。

今回は、ECサイトなどでよく使われる「ショッピングカート」のクラスを作成します。

データ(商品のリスト)と処理(追加、合計金額の計算)が1つのクラスに綺麗にまとまっていることを確認してください。

Reactなどのモダンなフロントエンド開発でクラスを使う際、初心者が必ずと言っていいほど直面するバグが「thisの消失(thisのスコープ外れ)」です。

クラスのメソッドを、別の関数にコールバックとして渡したり、ボタンのクリックイベント(onClick={cart.addItem}など)に直接渡したりすると、実行されるタイミングでthisが何を指しているのか分からなくなり、「this is undefined」というエラーでアプリがクラッシュします。

これを防ぐ実務のテクニックとして、メソッドを通常関数ではなく「アロー関数」で定義するという方法がよく使われます。

// カートに入れる商品の型定義
interface Product {
  name: string;
  price: number;
}

class ShoppingCart {
  // プロパティ:カートの中身(初期値は空の配列)
  private items: Product[] = []; 

  // コンストラクタ:今回は初期化時に特別なデータが不要なので省略可能です
  // constructor() {} 

  // ⭕ 実務のテクニック:メソッドを「アロー関数」で定義する
  // アロー関数にすることで、どこから呼び出されても `this` が必ずこのクラス自身を指すようになります(thisの固定化)
  addItem = (item: Product): void => {
    this.items.push(item);
    console.log(`${item.name} をカートに追加しました。`);
  };

  // 合計金額を計算するメソッド
  getTotalPrice = (): number => {
    // reduce関数を使って、配列内の price をすべて足し合わせる
    return this.items.reduce((total, item) => total + item.price, 0);
  };
}

// --- 実際にクラスを使ってみる ---

// 1. クラスからインスタンス(実体)を生成
const myCart = new ShoppingCart();

// 2. メソッドを呼び出してデータを操作
myCart.addItem({ name: "TypeScript入門書", price: 3000 });
myCart.addItem({ name: "高級キーボード", price: 15000 });

// 3. 結果を取得
console.log(`現在の合計金額は ${myCart.getTotalPrice()} 円です。`);
// 出力: 
// TypeScript入門書 をカートに追加しました。
// 高級キーボード をカートに追加しました。
// 現在の合計金額は 18000 円です。

// ❌ items は private なので、外から勝手に操作(破壊)される心配はありません
// myCart.items = []; // 🚨 エラー: プロパティ 'items' はプライベートです。

クラスの機能「継承(extends)」とスーパークラス

クラスの基本的な使い方(プロパティとメソッドの定義、インスタンス化)をマスターしたら、次に覚えるべきオブジェクト指向の強力な武器が「継承(けいしょう)」です。

インターネットで「プログラミング クラス 継承」と検索すると難しい説明が並びますが、要するに「すでに作ってある設計図(クラス)の機能を丸ごと引き継いで、新しい設計図を作る機能」のことです。

また、この引き継ぎ元となる大元のクラスのことを「プログラミング スーパー クラス(親クラス)」と呼びます。

ここでは、TypeScriptにおけるextendsキーワードを使った継承の仕組みと、実務で絶対に気をつけるべき注意点を解説します。

クラスの機能「継承(extends)」とスーパークラス
  • 既存のクラスを拡張する「継承」とは
  • 親クラス(スーパークラス)と子クラス(サブクラス)の関係

既存のクラスを拡張する「継承」とは

継承のメリットは「コードの重複(同じコードを何度も書くこと)を防ぐ」ことです。

例えば、「ユーザー(User)」クラスと「管理者(Admin)」クラスを作りたいとします。

管理者はユーザーの一種なので、名前やメールアドレスといったデータは共通しています。

この時、管理者クラスを一から書き直すのではなく、「ユーザーの機能を受け継ぎつつ(継承)、管理者専用の権限変更メソッドだけを追加する(拡張)」という作り方ができます。

これを実現するのがextends(拡張する)というキーワードです。

初心者が実務で一番やってしまう大失敗が、「何でもかんでも継承で解決しようとして、継承の階層を深くしすぎてしまうこと」です。

例:生き物クラス > 哺乳類クラス > 犬クラス > 柴犬クラス… といった何重もの継承

継承が深くなると、一番上のクラス(親)を少し修正しただけで、下にあるすべてのクラス(子孫)に予期せぬバグを引き起こす危険性があります(これを「基底クラスの脆弱性問題」と呼びます)。

現代のTypeScriptやReactの実務では、「継承は原則1階層(親と子)まで」に留め、複雑な機能は継承ではなく「クラスの合成(コンポジション)」で解決するのがベストプラクティスとされています。

// ⭕ 大元となるクラス(親クラス)
class User {
  constructor(public name: string, public email: string) {}

  login(): void {
    console.log(`${this.name} がログインしました。`);
  }
}

// ⭕ 継承を使って新しいクラスを作る(子クラス)
// `extends User` と書くことで、Userのプロパティとメソッドを全て引き継ぐ!
class AdminUser extends User {
  // AdminUser独自のメソッドを追加
  deleteAccount(targetName: string): void {
    console.log(`管理者権限で ${targetName} のアカウントを削除しました。`);
  }
}

// AdminUserのインスタンスを作成
const admin1 = new AdminUser("田中", "tanaka@example.com");

// 親クラス(User)から引き継いだメソッドがそのまま使える!
admin1.login(); // 出力: 田中 がログインしました。

// もちろん独自のメソッドも使える!
admin1.deleteAccount("スパムユーザー"); 

親クラス(スーパークラス)と子クラス(サブクラス)の関係

継承関係にあるクラスにおいて、それぞれの呼び方は以下のようになります。

  • 親クラス(スーパークラス / 基底クラス)
    機能を引き継がせる「元」となるクラス。(先ほどの例のUser
  • 子クラス(サブクラス / 派生クラス)
    親の機能を引き継いで誕生した「新しい」クラス。(先ほどの例のAdminUser

子クラスの中で親クラスの機能にアクセスしたい場面は多々あります。

その際に絶対に覚えなければならないキーワードが「super(スーパー)」です。

TypeScriptで子クラスを作る際、初心者が100%つまずくエラーが「子クラスで独自のconstructorを書いたのに、中でsuper()を呼び忘れる」または「super()を呼ぶ前にthisを使おうとする」ことです。

子クラスで新しく初期化処理(コンストラクタ)を追加する場合、「まずは何より先に親クラス(スーパークラス)の初期化処理を終わらせなければならない」という絶対的なルールがあります。

そのため、コンストラクタの一番最初の行でsuper()を実行しないと、TypeScriptが即座にエラーを出してプログラムの実行を止めてくれます。

// スーパークラス(親クラス)
class Character {
  constructor(public name: string, public hp: number) {}

  attack(): void {
    console.log(`${this.name} の基本攻撃!`);
  }
}

// サブクラス(子クラス)
class Wizard extends Character {
  // 子クラス独自のプロパティ(魔力: mp)を追加したい場合
  constructor(name: string, hp: number, public mp: number) {
    // 🚨 【最重要】子クラスでconstructorを書く場合は、必ず一番最初に super() を呼ぶ!
    // これにより、親クラスの constructor(name, hp) が実行されて準備が整う
    super(name, hp);
    
    // ❌ super() より前に this.mp = mp; などを書くとコンパイルエラーになります
  }

  // ⭕ メソッドの「オーバーライド(上書き)」
  // 親と同じ名前のメソッドを書くと、子クラス専用の処理に上書き(カスタマイズ)できる
  attack(): void {
    if (this.mp >= 10) {
      this.mp -= 10;
      console.log(`${this.name} は魔法を唱えた!(残りMP: ${this.mp})`);
    } else {
      // super.メソッド名() で、親クラスの元のメソッドを呼び出すことも可能!
      console.log("MPが足りない!");
      super.attack(); 
    }
  }
}

const gandalf = new Wizard("ガンダルフ", 100, 50);

// Wizardクラスで上書き(オーバーライド)された攻撃が出る
gandalf.attack(); // 出力: ガンダルフ は魔法を唱えた!(残りMP: 40)

クラスの命名規則と名前の付け方

クラスの仕組みを理解し、いざ自分で設計図を作ろうとした時に多くの人が悩むのが「クラスの名付け方」です。

そもそも「クラス名とは何か?」と言えば、その設計図から生み出されるモノ(インスタンス)の「種族名」や「役割名」を表す非常に重要なラベルです。

命名の良し悪しは、そのままシステム全体の「読みやすさ(可読性)」に直結します。

また、コードは世界共通であるため、プログラミング クラス 英語 で命名するのが絶対のルールです。

ローマ字(例:Taiyaki)は学習用としてはわかりやすいですが、実務のクラス名としては基本的にNGとされます。

ここでは、TypeScript実務で必須となるクラス名のルールと現場で嫌われる「悪い名前」、歓迎される「良い名前」の具体例を解説します。

クラスの命名規則と名前の付け方
  • クラス名は「パスカルケース」が基本ルール
  • 良いクラス名・悪いクラス名の例

クラス名は「パスカルケース」が基本ルール

TypeScript(および多くのオブジェクト指向言語)において、クラス名には「パスカルケース(PascalCase)」を使うという世界共通の暗黙のルールがあります。

パスカルケースとは、「すべての単語の頭文字を大文字にして繋げる」書き方です。

変数や関数で使われる「キャメルケース(camelCase:最初の単語だけ小文字、次から大文字)」とは明確に区別されます。

  • 変数・関数(キャメルケース)userProfileshoppingCartcalculateTotal
  • クラス(パスカルケース)UserProfileShoppingCartPaymentProcessor

初心者が実務でよくやってしまうのが、「クラス名に『動詞』を使ってしまう」という命名ミスです。

クラスはあくまでモノ(設計図)」であるため、「名詞」でなければなりません。

動詞(例:class Calculateclass SendEmail)にしてしまうと、それが「関数」なのか「クラス」なのかパッと見で判別できなくなり、他のエンジニアをひどく混乱させます。

// ❌ 初心者がやりがちな悪い命名ルール(小文字始まり、動詞)
class calculatePrice {
  // ...
}
// これだと関数(キャメルケース&動詞)に見えてしまい、new する時に違和感がすごい
const calc = new calculatePrice(); 


// ⭕ 実務での正しい命名ルール(大文字始まり、名詞)
// 「価格計算機」という【モノ】としてパスカルケースで命名する
class PriceCalculator {
  // メソッド(処理)には動詞を使う
  calculate(price: number): number {
    return price * 1.1;
  }
}

// クラス名が大文字始まりの名詞であるため、new して実体を作るのが自然に読める
const calculator = new PriceCalculator();
const total = calculator.calculate(1000);

良いクラス名・悪いクラス名の例

パスカルケースと名詞のルールを守っていても、実務では「バグを生みやすい悪い名前」が存在します。

良いクラス名とは「そのクラスが『何をするモノ』なのか、名前を見ただけで機能が限定できる名前」です。

逆に悪いクラス名とは「何でも入りそうな、曖昧で広すぎる名前」です。

実務のコードレビューで最も厳しく指摘されるのが、「Manager」「Data」「Info」「Util」といった、意味が広すぎる単語(マジックワード)を付けたクラスです。

例えばUserDataUserManagerというクラスを作ってしまうと、「ユーザーに関するものなら何でも入れていいゴミ箱」状態になります。

名前が広すぎるせいで、メール送信処理からパスワード暗号化まで様々な機能が1つのクラスに詰め込まれ、最終的に数千行の「神クラス(God Class)」という最悪のアンチパターンを生み出します。

クラス名は「これ以外の機能は入れません」と宣言するくらい、具体的で狭い名前(単一責任の原則)にするのがよいです。

// ❌ 悪いクラス名(何でも入る「Manager」や「Data」)
class UserManager {
  // ユーザーの作成、ログイン処理、メール送信、果ては商品のお気に入り登録まで...
  // どんどん機能が追加され、誰にも全貌が把握できない巨大なクラスに成長してしまう
}

// ⭕ 良いクラス名(役割が1つに限定された具体的な名前)
// 「ユーザーを登録(作成)するためのクラス」だと一目でわかる
class UserCreator {
  createUser(name: string): void { /* ... */ }
}

// 「ユーザーの認証(ログイン・ログアウト)だけを担当するクラス」
class UserAuthenticator {
  login(): void { /* ... */ }
  logout(): void { /* ... */ }
}

// 「ユーザーへの通知(メール等)だけを担当するクラス」
class UserNotifier {
  sendWelcomeEmail(): void { /* ... */ }
}

// --- 実務での使用イメージ ---
// それぞれのクラスが小さく独立しているため、テストや仕様変更が圧倒的に楽になる
const auth = new UserAuthenticator();
auth.login();

オブジェクト指向のクラス設計と「クラス図」

クラスの文法や書き方を覚えた後に、実務で多くのエンジニアが直面する最も高い壁が「クラス設計」です。

「文法はわかるけど、どのデータをどのクラスに入れればいいのか分からない」「クラス設計のように、複雑なシステムをどう構築すればいいか迷う」という悩みは尽きません。

良いクラス設計ができるかどうかで、将来的なバグの少なさや、機能追加のしやすさが劇的に変わります。

ここでは、初心者が迷いがちなクラスの分割方法、設計図である「クラス図」の書き方、避けるべきアンチパターンまでをTypeScriptの実例とともに解説します。

オブジェクト指向のクラス設計と「クラス図」
  • クラス設計とは?どうやってクラス分けをするべきか
  • クラスの関係を可視化する「クラス図」の書き方と意味
  • 【アンチパターン】何でもできる「神クラス」は避ける

クラス設計とは?どうやってクラス分けをするべきか

クラス設計の第一歩は、「クラス化(クラスとして定義すること)」する対象を見つけ、「クラス分け(分割)」を適切に行うことです。

「クラスの分け方」の基本となるルールは、「現実世界の『名詞(モノ)』をクラスにし、そのモノが持つ『データ(属性)』をプロパティに、『動詞(振る舞い)』をメソッドにする」という考え方です。

例えば、RPGゲームを作る場合、「剣で攻撃する」「ポーションで回復する」といった処理をすべて1つの場所に書くのではなく、「プレイヤー」「武器」「アイテム」という独立したクラスに分割し、それぞれに責任を持たせます。

初心者が実務で一番悩むのが、「動詞(処理)をそのままクラスにしてしまう」という設計ミスです。

例えば「ログイン処理」を作りたい時に、class LoginProcessorのように処理そのものをクラス化してしまうと、オブジェクト指向の恩恵(データと処理の一体化)を受けられず、単なる「関数の集まり」になってしまいます。

実務のベストプラクティスは、あくまでUser(名詞)というクラスを作り、その中にlogin()(動詞)というメソッドを持たせることです。

// ❌ 悪いクラス分け:処理(動詞)を中心にクラスを作ってしまっている
class BattleSystem {
  // プレイヤーと敵のデータ、攻撃の計算などを全部1箇所で管理しようとしている
  playerHp: number = 100;
  enemyHp: number = 50;

  playerAttack(weaponDamage: number): void {
    this.enemyHp -= weaponDamage;
  }
}

// ⭕ 良いクラス分け:名詞(モノ)を中心に分割し、責任を分散させる
// 1. 武器クラス
class Weapon {
  constructor(public name: string, public damage: number) {}
}

// 2. キャラクタークラス(プレイヤーも敵もこれをベースにできる)
class Character {
  constructor(public name: string, public hp: number) {}

  // キャラクター自身が「攻撃される」振る舞いを持つ
  takeDamage(amount: number): void {
    this.hp -= amount;
    console.log(`${this.name} は ${amount} のダメージを受けた!残りHP: ${this.hp}`);
  }
}

// クラスを組み合わせて使う(Weaponクラスのデータを、Characterクラスに作用させる)
const sword = new Weapon("鉄の剣", 15);
const slime = new Character("スライム", 50);

slime.takeDamage(sword.damage); // 出力: スライム は 15 のダメージを受けた!残りHP: 35

クラスの関係を可視化する「クラス図」の書き方と意味

設計した複数のクラスが、お互いにどう連携しているかを整理・共有するために使われるのが「クラス図」です。

「クラス図とは何か?」というと、UML(統一モデリング言語)という世界共通の規格で定められた設計図の一種です。

「クラス図の書き方」の基本ルールは、1つのクラスを四角形(ボックス)で描き、それを上から順に「クラス名」「属性(プロパティ)」「操作(メソッド)」の3つの部屋に分けて記述します。

そして、クラス同士を線や矢印で繋いで、継承や依存関係を示します。

初心者が陥りがちな実務の罠が、「コーディングを始める前に、完璧で厳密なクラス図を作ろうと時間をかけすぎてしまうこと」です。

現代のWeb開発(アジャイル開発など)では、最初から完璧な設計は不可能です。

厳密なUMLのルールに縛られて立ち止まるよりも、ホワイトボードやツールを使って「どのクラスがどのデータを持つか」をチームでザックリ共有する「ラフなクラス図」を書くことの方が実務では重視されます。

// --- クラス図をTypeScriptのコードに落とし込む例 ---

// 【クラス図の記述イメージ】
// -----------------------------
// |        Item (Interface)   | <- まず共通の「型」を決める
// -----------------------------
// | + name: string            |
// | + use(): void             |
// -----------------------------
//            △ (実装/implements)
// -----------------------------
// |        Potion (Class)     | <- クラス図の設計通りにコードを書く
// -----------------------------
// | - healAmount: number      |
// -----------------------------
// | + use(): void             |
// -----------------------------

// 1. インターフェース(クラスの必須ルールを定めた設計書)
interface Item {
  name: string;
  use(): void;
}

// 2. クラス図に基づいて Potion クラスを実装(implements)
class Potion implements Item {
  // `+` は public, `-` は private を表す(クラス図の書き方ルール)
  public name: string;
  private healAmount: number;

  constructor(name: string, healAmount: number) {
    this.name = name;
    this.healAmount = healAmount;
  }

  public use(): void {
    console.log(`${this.name} を使った!HPが ${this.healAmount} 回復した!`);
  }
}

const redPotion = new Potion("赤い薬", 30);
redPotion.use();

【アンチパターン】何でもできる「神クラス」は避ける

クラス設計において、実務で絶対にやってはいけない最大のアンチパターンが、「神クラス」を生み出してしまうことです。

神クラスとは、「あまりにも多くのデータと処理を抱え込みすぎた、巨大で万能なクラス」のことです。

例えば、SystemManagerAppControllerといった名前が付けられやすく、データベースの保存から画面の描画、ユーザー入力の処理まで、全てを1つのクラスでやってしまいます。

なぜ神クラスが実務で大問題になるのでしょうか?

それは「変更が怖くて誰も触れなくなるから」です。 数千行にも及ぶ神クラスでは、1つのプロパティを変更しただけで、全く関係のない別の機能がバグるという事態が頻発します。

また、チーム開発において複数人が同時に同じファイルを編集するため、毎日のように「コードの競合(マージコンフリクト)」が発生し、開発スピードが著しく低下します。

クラスは「単一責任の原則(1つのクラスは、1つの役割だけを持つべき)」に従い、小さく分割するのがよいです。

// ❌ 最悪のアンチパターン:神クラス(God Class)
class GameManagerGod {
  // あらゆるデータを1箇所に詰め込んでいる
  playerScore: number = 0;
  enemyList: string[] = [];
  databaseConnection: boolean = true;
  isSoundMuted: boolean = false;

  // あらゆる処理を1つのクラスでやろうとしている(数千行になる未来が見える)
  updateScore(): void { /* ... */ }
  spawnEnemy(): void { /* ... */ }
  saveToDatabase(): void { /* ... */ }
  playBgm(): void { /* ... */ }
}

// ⭕ 実務での正しい設計:役割ごとに小さなクラスに分割する(単一責任の原則)
class ScoreManager {
  private score: number = 0;
  addScore(points: number): void {
    this.score += points;
    console.log(`スコア追加: ${points} (合計: ${this.score})`);
  }
}

class EnemySpawner {
  spawn(enemyType: string): void {
    console.log(`${enemyType} が出現した!`);
  }
}

class AudioController {
  play(trackName: string): void {
    console.log(`BGM再生: ${trackName}`);
  }
}

// 必要な時に、必要なクラス(部品)だけを呼び出して使う
const scoreManager = new ScoreManager();
const enemySpawner = new EnemySpawner();

enemySpawner.spawn("ゴブリン");
scoreManager.addScore(100);

実務でよく見る特殊な役割を持つクラス

これまでの章で、クラスは「ユーザー」や「武器」といった「名詞(モノ)」をベースに作ると解説しました。

しかし、実際のシステム開発の現場では、目に見えるモノだけでなく、「システム上の特定の役割を持った裏方のクラス」が大量に登場します。

ここでは、実務の設計でよく使われる「サービスクラス」「マネージャークラス」、「ラッパークラス」という特殊な役割を持つクラスについて解説します。

実務でよく見る特殊な役割を持つクラス
  • 処理をまとめる「サービスクラス・マネージャークラス」
  • 型を変換・ラップする「ラッパークラス」

処理をまとめる「サービスクラス・マネージャークラス」

Webアプリケーションなどの複雑なシステムでは、データの保存、メール送信、決済処理など、「モノ(データ)」ではなく「一連の処理の流れ(ビジネスロジック)」をひとまとめにしたい場面が多々あります。

こういった処理を担当するのが「サービスクラス」や「マネージャー クラス」です。

例えば、ユーザー登録処理であればUserRegistrationService、画面の表示状態を管理するのであればDisplayManagerといった名前が付けられます。

これらは「特定の業務や手続きを専門にこなす窓口」のような役割を果たします。

初心者が実務でよく陥る設計ミスが、「サービスクラスの中に、状態を持たせてしまうこと」です。

サービスクラスは原則として「状態を持たない(ステートレスである)」べきです。

もしPaymentServiceの中にcurrentTotalAmountのような変動するデータを持たせてしまうと、複数のユーザーが同時にアクセスした際にデータが混ざってしまい、大事故を引き起こす原因になります。

サービスクラスは「引数でデータを受け取り、処理結果を返すだけ」の純粋な設計にするのがよいです。

// ❌ 悪いサービスクラスの例(状態を持ってしまっている)
class BadPaymentService {
  // 🚨 サービスが直接データを持っているため、複数回呼ばれるとバグる危険がある
  private amount: number = 0; 

  processPayment(price: number) {
    this.amount += price;
    console.log(`${this.amount}円を決済しました。`);
  }
}

// ⭕ 良いサービスクラスの例(状態を持たない・ステートレス)
// 必要なデータはすべて「引数」として受け取る
class PaymentService {
  // 決済処理という「振る舞い」だけを提供する
  processPayment(userId: string, price: number): boolean {
    console.log(`ユーザー[${userId}]の ${price} 円の決済処理を開始します...`);
    // 外部の決済APIなどを叩く処理がここに入る
    console.log("決済完了!");
    return true; 
  }
}

// 実務での使い方(インスタンス化して処理を委譲する)
const paymentService = new PaymentService();
paymentService.processPayment("user_123", 5000);

型を変換・ラップする「ラッパークラス」

実務で必須となるもう一つの特殊なクラスが「ラッパークラス」です。

「ラップ(Wrap:包む)」という言葉の通り、既存の複雑な機能や外部のライブラリを「自分の使いやすい形に包み込んで隠す」クラスです。

例えば、ブラウザにデータを保存するlocalStorageという機能があります。

これをそのまま使うと文字列しか保存できず不便ですが、ラッパークラスを作って包み込むことで、「自動でJSONに変換して保存してくれる便利な独自の保存機能」へと生まれ変わらせることができます。

ここで実務上よく発生するミスが、「外部ライブラリをラップした意味をなくしてしまう(型の漏洩)」ことです。

外部ライブラリの専用の型(Type)を、ラッパークラスの戻り値としてそのまま外に返してしまうと、結局プロジェクト全体がその外部ライブラリに依存してしまいます。

将来、「別のライブラリに乗り換えよう」となった時に、システム全体を書き直す羽目になります。

ラッパークラスを作る際は、「内部の複雑な仕様や型を、外の世界(呼び出し元)に絶対に漏らさない」ことが最重要です。

// ⭕ 実務で役立つラッパークラスの例(localStorage を使いやすく安全に包む)
class StorageWrapper {
  // 保存する処理(オブジェクトを自動で文字列のJSONに変換して包み込む)
  setItem(key: string, value: any): void {
    try {
      const stringValue = JSON.stringify(value);
      localStorage.setItem(key, stringValue);
    } catch (error) {
      console.error("保存に失敗しました", error);
    }
  }

  // 取り出す処理(文字列のJSONを自動で元のオブジェクトに戻す)
  getItem<T>(key: string): T | null {
    const item = localStorage.getItem(key);
    if (!item) return null;

    try {
      return JSON.parse(item) as T; // TypeScriptのジェネリクスを使って型を復元
    } catch (error) {
      console.error("データの読み込みに失敗しました", error);
      return null;
    }
  }
}

// --- ラッパークラスの使用例 ---
const myStorage = new StorageWrapper();

// 複雑なオブジェクトもそのまま渡すだけで安全に保存できる(内部の面倒なJSON変換は隠蔽されている)
const userSettings = { theme: "dark", notifications: true };
myStorage.setItem("settings", userSettings);

// 取り出す時も、元の型(オブジェクト)として綺麗に返ってくる
const loadedSettings = myStorage.getItem<{ theme: string; notifications: boolean }>("settings");
if (loadedSettings) {
  console.log(`現在のテーマ: ${loadedSettings.theme}`); // 出力: 現在のテーマ: dark
}

まとめ

分かりやすいようにまとめを記載します。

本記事のまとめ
  • クラスはデータと処理をひとまとめにするための「設計図」である。
  • クラス(設計図)からnewを用いて生成された実体を「インスタンス」と呼ぶ。
  • クラスが保持する状態やデータ(変数)を「プロパティ」、振る舞いや処理(関数)を「メソッド」と定義する。
  • インスタンス生成時に一度だけ実行される初期化処理は「コンストラクタ」で記述する。
  • 「継承(extends)」を用いることで、既存の親クラスの機能を引き継ぎつつ拡張できる。
  • クラスの命名は「パスカルケース(先頭大文字の英単語)」かつ「名詞」とするのが標準的なルールである。
  • クラス設計では単一責任の原則に従い、機能が肥大化した「神クラス」を避けて役割ごとに小さく分割する。

よくある質問(FAQ)

プログラミングのクラスとは簡単に言うと何ですか?

プログラムにおける「設計図」のことです。

あるモノ(ユーザーやキャラクターなど)が持つデータ(名前、HPなど)と、それに対する処理(攻撃する、保存するなど)を1つにまとめた枠組みです。

この設計図を作ることで、同じ特徴を持つ実体をいくつでも効率よく作成できるようになります。

「クラス」と「インスタンス」は何が違うのですか?

「設計図」そのものか、「その設計図を元に作られた具体的な実体」かという違いです。

  • クラス
    たい焼きの「型」そのものです。
    どんな味のたい焼きが作れるかというルールが決まっています。
  • インスタンス
    その型から焼き上がった「実際のたい焼き」です。
    インスタンス化(newすること)することで、初めてデータを持って動く存在になります。
なぜクラスを使う必要があるのですか?

コードを整理し、大規模な開発でもバグを減らして管理しやすくするためです。

データとそれを扱う処理を1つにまとめることで、プログラムの見通しが良くなります。

また、同じような処理を何度も書く必要がなくなり、後から仕様変更があった場合でも、そのクラスの中身を修正するだけでシステム全体に反映させることができるため、保守性が大幅に向上します。

クラスを使わないプログラミングとは何が違いますか?

「データ」と「処理」がバラバラに管理される点が大きく違います。

クラスを使わない場合、データは変数として保存し、処理は関数として別々に作ることになります。

規模が小さいプログラムならこれでも問題ありませんが、規模が大きくなると「このデータはどの関数で使うんだっけ?」「このデータを勝手に書き換えてはいけない場所はどこだっけ?」という混乱を招き、バグの温床になります。

クラスはこれを整理整頓してくれます。

継承(extends)は何のために使うのですか?

既存の機能を流用して、新しい機能を効率よく作るために使います。

例えば「ユーザー」という基本クラスがある場合、それを継承して「管理者」クラスを作れば、名前やログインといった基本機能を一から書く必要はありません。

基本機能は親クラスに任せ、自分には「管理者専用の権限変更」といった独自の機能だけを追加すれば良いため、コードを大幅に簡略化できます。

// 親クラスの機能を継承し、拡張するイメージ
class Admin extends User {
  // Userの機能(名前管理など)はそのまま使える
  // 管理者専用の機能だけを追加すれば良い
  deleteUser() { /* ... */ }
}

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次