JavaScript | ES6+ 文法:クラス構文 – クラス設計の考え方

JavaScript JavaScript
スポンサーリンク

クラス設計の考え方とは何か

クラス設計は、
「コードをどう書くか」より先に、
「このクラスは何者で、何ができて、どこまで面倒を見るのか」を決める作業です。

JavaScript の class は、
単に「便利な文法」ではなく、
「自分の考え方をコードに焼き付ける枠組み」です。

ここが重要です。
先に設計(役割・境界・責任)を決めておくと、
constructor、メソッド、getter / setter、プライベートフィールドなど
ES6+ の機能が「どこでどう使えばよいか」が自然に決まってきます。

このあと、初心者が意識しておくと強い
クラス設計の考え方を、例を交えながら順番に深掘りしていきます。

まず「何のクラスか」を一言で言えるようにする

一文で説明できるクラスにする

クラス設計の最初の一歩は、
「このクラスを人に説明するとき、どう一言で言えるか」です。

例えば:

「ユーザーの情報とログイン回数を管理するクラス」
「銀行口座の残高と入出金ルールを扱うクラス」
「ゲームのプレイヤーキャラの状態と行動を表すクラス」

この一文がはっきりしていないクラスは、
すぐに「何でも屋」になって壊れていきます。

簡単な例を見てみます。

class User {
  constructor(name) {
    this.name = name;
    this.loginCount = 0;
  }

  login() {
    this.loginCount++;
  }

  describe() {
    console.log(`${this.name}(ログイン回数: ${this.loginCount})`);
  }
}
JavaScript

このクラスは、
「ユーザーの名前とログイン回数を管理し、表示できるクラス」
と一言で言えます。

ここが重要です。
自分に問いかけてください。
「このクラスは、一言で言うと何?」
それに答えられないなら、そこから考え直す価値があります。

状態(プロパティ)と振る舞い(メソッド)をセットで考える

「名詞っぽいもの」と「動詞っぽいもの」を分ける

クラスはざっくり言うと、

状態(プロパティ)
振る舞い(メソッド)

のセットです。

銀行口座を例にすると、こうなります。

class BankAccount {
  constructor(owner, initialBalance = 0) {
    this.owner = owner;     // 状態
    this.balance = initialBalance; // 状態
  }

  deposit(amount) {        // 振る舞い
    this.balance += amount;
  }

  withdraw(amount) {       // 振る舞い
    if (this.balance < amount) {
      console.log("残高不足です");
      return false;
    }
    this.balance -= amount;
    return true;
  }

  show() {                 // 振る舞い
    console.log(`${this.owner} の残高: ${this.balance}円`);
  }
}
JavaScript

状態は「そのオブジェクトが持っている情報」です。
振る舞いは「その情報をどう扱うのか」です。

ここが重要です。
「名詞っぽいもの」はプロパティに、
「動詞っぽいもの」はメソッドにする、
という意識を持つだけで、クラスの形が一気に整います。

状態を外から直接いじらせるか、メソッドからだけにするか

もう一歩進むと、こう考えます。

「本当に account.balance を外から直接書き換えさせていいのか?」

多くの場合、ダメなはずです。
残高は「入金」「出金」というルールを通してしか変わってほしくない。

そこで、プライベートフィールドや getter を使って
「状態と振る舞い」をもう一段きれいに分離できます。

class SafeBankAccount {
  #balance;

  constructor(owner, initialBalance = 0) {
    this.owner = owner;
    this.#balance = initialBalance;
  }

  get balance() {
    return this.#balance;
  }

  deposit(amount) {
    if (amount <= 0) throw new Error("金額がおかしい");
    this.#balance += amount;
  }

  withdraw(amount) {
    if (amount <= 0) throw new Error("金額がおかしい");
    if (this.#balance < amount) throw new Error("残高不足");
    this.#balance -= amount;
  }
}
JavaScript

外からは account.balance で見ることはできても、
account.balance = 999999 のように変えることはできません。

ここが重要です。
「この値はどこからでも書き換えてよいか?」
「それとも、クラスのルールを通したときだけ書き換えたいか?」
この問いで、「公開プロパティ」か「プライベート+メソッド経由」かを決めます。

インスタンス生成と constructor の設計

「生成に必要な情報だけ」を引数にする

constructor には、
「そのクラスのインスタンスを作るために最低限必要な情報」
だけを受け取らせるのが基本です。

プレイヤーキャラなら、例えば:

class Player {
  constructor(name, maxHp = 100) {
    this.name = name;
    this.hp = maxHp;
    this.maxHp = maxHp;
  }

  damage(amount) {
    this.hp = Math.max(0, this.hp - amount);
  }

  heal(amount) {
    this.hp = Math.min(this.maxHp, this.hp + amount);
  }
}
JavaScript

ここで constructor に「それ以外の細かいフラグ」や「ゲーム全体の設定」などを
どんどん突っ込み始めると、すぐに設計が崩れます。

ここが重要です。
constructor の引数を見たとき、
「このクラスを使う人にとって、これだけ知っていれば作れる」
という最小限になっているかどうかを意識してください。

デフォルト値で「よくあるケース」を楽に

よくある初期状態にはデフォルト値をつけておくと、
クラスが一気に使いやすくなります。

class Button {
  constructor(label, color = "blue", size = "medium") {
    this.label = label;
    this.color = color;
    this.size = size;
  }
}

const okButton = new Button("OK");
const cancelButton = new Button("Cancel", "red");
JavaScript

どこまでを引数で受け取り、どこからをデフォルトにするかも設計です。
「よく使われる形」が new しやすくなっているかを考えます。

メソッドの設計と「責任範囲」の決め方

一つのメソッドに「一つの目的」

良いメソッドは、名前を見ただけで「何をするか」が分かり、
それ以上のことはしません。

例えば、ユーザーのクラスで:

class User {
  constructor(name) {
    this.name = name;
    this.loginCount = 0;
  }

  login() {
    this.loginCount++;
  }

  resetLoginCount() {
    this.loginCount = 0;
  }

  rename(newName) {
    this.name = newName;
  }
}
JavaScript

login は「ログイン回数を増やすだけ」、
resetLoginCount は「ログイン回数をリセットするだけ」です。

ここが重要です。
メソッドに「ついでにこれもやっとくか」と処理を足し始めると、
すぐに「何でも屋メソッド」が生まれてしまいます。
「名前に書いてあることだけをやる」と割り切ると、クラス全体が整います。

クラスの外でやるべきことを、クラスに持ち込まない

例えば、User クラスに「API に保存する」「DOM を更新する」などの処理を
直接書き始めると、あっという間に肥大化します。

// よくない例
class User {
  saveToServer() { /* fetch で POST する */ }
  renderToDom() { /* document.querySelector で… */ }
}
JavaScript

こういう処理は「ユーザー」という概念ではなく、
「通信」「表示」という別の責任です。

初心者のうちは、
「このメソッドは、このクラスの“データそのもの”に対する操作か?」
「それとも、別レイヤーの処理を持ち込んでいないか?」
と自問してみてください。

カプセル化とインターフェースの設計

内部の実装は隠し、外には「必要な窓口だけ」を出す

プライベートフィールド(#)と getter / setter は、
クラス設計の「武器」です。

ユーザーを例にすると:

class User {
  #passwordHash;

  constructor(name, password) {
    this.name = name;
    this.#passwordHash = this.#hash(password);
  }

  #hash(text) {                 // 内部専用
    return `hash:${text}`;
  }

  checkPassword(password) {      // 公開したい窓口
    return this.#passwordHash === this.#hash(password);
  }
}
JavaScript

外からは checkPassword だけを呼べますが、
中でどんなハッシュの仕組みを使っているかは一切見えません。

ここが重要です。
「外に見せるメソッド・プロパティ」をインターフェースと言います。
インターフェースはできるだけ少なく、シンプルであるほど良いです。
内部がどれだけ複雑でも、外から見える顔がスッキリしていれば使う側は迷いません。

「読み取り専用」「書き込み禁止」もインターフェースで表現する

getter だけを用意して setter を置かない、というのも設計です。

class Order {
  constructor(price, quantity) {
    this.price = price;
    this.quantity = quantity;
  }

  get total() {             // 読み取り専用
    return this.price * this.quantity;
  }
}
JavaScript

total は計算結果であり、「外から勝手に書き換えられるべきではない値」です。
このように、「どう使われるべきか」をインターフェースに反映させていきます。

継承を使うかどうかの判断

「〜の一種」と自然に言えるか

継承(extends)を使うときの大事な判断基準は、
「本当に “〜の一種” と言えるか」です。

例えば:

ユーザーと管理者ユーザー
動物と犬
図形と円・四角形

こういった関係では、継承が自然です。

class User {
  constructor(name) {
    this.name = name;
  }

  describe() {
    console.log(`${this.name}(一般ユーザー)`);
  }
}

class AdminUser extends User {
  constructor(name, permissions = []) {
    super(name);
    this.permissions = permissions;
  }

  describe() {
    console.log(`${this.name}(管理者)権限: ${this.permissions.join(", ")}`);
  }

  hasPermission(p) {
    return this.permissions.includes(p);
  }
}
JavaScript

ここが重要です。
なんとなくコードが似ているからといって、すぐに extends してはダメです。
設計として「〜の一種」と言える関係があるときにだけ使うと、
クラスの関係がきれいに保てます。

共通部分を「親クラス」に押し上げる感覚

継承を使うなら、
「全員に共通している状態と振る舞い」を親クラスに上げます。

図形を例にすると:

class Shape {
  area() {
    return 0;
  }

  describe() {
    console.log(`面積: ${this.area()}`);
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  area() {
    return Math.PI * this.radius * this.radius;
  }
}
JavaScript

describe は全ての図形に共通なので親。
area の計算方法は種類ごとに違うので子クラスでオーバーライド。
こういう切り分けを意識できると、継承設計が楽しくなります。

小さな例で「クラス設計」を通してみる

例:Todo アイテムのクラスを設計する

一言で言うと「Todo の一件を表すクラス」。
状態は何か、振る舞いは何かを考えます。

状態:id、タイトル、完了フラグ、作成日時、更新日時
振る舞い:完了にする、未完了に戻す、タイトル変更

class TodoItem {
  #title;
  #done;
  #createdAt;
  #updatedAt;

  constructor(id, title) {
    this.id = id;
    this.#title = title;
    this.#done = false;
    const now = Date.now();
    this.#createdAt = now;
    this.#updatedAt = now;
  }

  get title() {
    return this.#title;
  }

  set title(newTitle) {
    if (!newTitle) throw new Error("タイトルは空にできません");
    this.#title = newTitle;
    this.#updatedAt = Date.now();
  }

  get done() {
    return this.#done;
  }

  get createdAt() {
    return this.#createdAt;
  }

  get updatedAt() {
    return this.#updatedAt;
  }

  complete() {
    if (!this.#done) {
      this.#done = true;
      this.#updatedAt = Date.now();
    }
  }

  reopen() {
    if (this.#done) {
      this.#done = false;
      this.#updatedAt = Date.now();
    }
  }

  describe() {
    console.log(
      `[#${this.id}] ${this.#done ? "[x]" : "[ ]"} ${this.#title}`
    );
  }
}
JavaScript

このクラスの設計ポイントを言葉にすると:

「Todo の1件を表し、タイトル・完了状態・作成日時・更新日時を持つ」
「タイトル変更・完了・再オープンの操作を提供する」
「タイトルや完了状態の変更は、必ず updatedAt を更新するルールになっている」
「内部の生データは # で隠し、getter / setter を通して外に出す」

こういう「説明」がすらすらできるクラスは、だいたい良い設計になっています。

まとめ

クラス設計の考え方を、言い切ります。

クラスは、
「その世界の中の“ひとつの概念”を、コード上で表現するための器」です。

その器に対して、

このクラスは一言で何者か
どんな状態(プロパティ)を持つべきか
どんな振る舞い(メソッド)を提供すべきか
外からどう見せて、何を隠すべきか(カプセル化)
他のクラスと「〜の一種」という関係があるか(継承)

を考えていくのが「クラス設計」です。

ES6+ の class 構文(constructor、メソッド定義、省略記法、static、継承、super、プライベートフィールド、getter / setter)は、
この設計をコードに落とし込むための部品です。

いきなり完璧を目指す必要はありません。
最初は小さなクラスを一つ選んで、

一言の説明を書いてみる
状態と振る舞いを書き出してみる
外から触ってほしくないものに # をつけてみる
読み取り専用にしたいものに getter をつけてみる

このあたりから始めてみてください。

「ただ動くコード」から「意味のあるクラス」に変わった瞬間、
自分のコードが急に“自分のもの”になった感覚が出てきます。

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