クラス設計の考え方とは何か
クラス設計は、
「コードをどう書くか」より先に、
「このクラスは何者で、何ができて、どこまで面倒を見るのか」を決める作業です。
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;
}
}
JavaScriptlogin は「ログイン回数を増やすだけ」、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;
}
}
JavaScripttotal は計算結果であり、「外から勝手に書き換えられるべきではない値」です。
このように、「どう使われるべきか」をインターフェースに反映させていきます。
継承を使うかどうかの判断
「〜の一種」と自然に言えるか
継承(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;
}
}
JavaScriptdescribe は全ての図形に共通なので親。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 をつけてみる
このあたりから始めてみてください。
「ただ動くコード」から「意味のあるクラス」に変わった瞬間、
自分のコードが急に“自分のもの”になった感覚が出てきます。
