概要
JavaScriptにおけるクラス設計は、ただの構文糖ではない。
それは**“責務を定義し、オブジェクトの振る舞いを明示的に構造化するための設計戦略”**である。
継承(extends)を使うのか、委譲(composition)でまとめるのか。
これは機能の共通化だけでなく、可読性・拡張性・責任の局所性を左右する重大な設計判断だ。
本稿では、JavaScriptにおけるクラス設計の原則、継承と委譲の使い分け、責務の分割戦略を包括的に整理する。
1. クラスの基本構造と責務定義
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
greet() {
return `Hello, ${this.name}`;
}
}
- ✅ 状態(プロパティ)と振る舞い(メソッド)をひとまとめに定義
- ✅ クラス1つにつき「1つの責任」が原則(SRP)
2. 継承:親子関係で構造を拡張する
class Admin extends User {
constructor(name) {
super(name, 'admin');
}
hasAccess() {
return true;
}
}
- ✅ is-a 関係が成り立つ場合のみ採用
- ✅ 基本クラスの振る舞いを 拡張・オーバーライドできる
3. 委譲(Composition):機能の再利用と柔軟性の確保
class Logger {
log(msg) {
console.log(`[LOG]: ${msg}`);
}
}
class Task {
constructor(name, logger) {
this.name = name;
this.logger = logger;
}
run() {
this.logger.log(`Running ${this.name}`);
}
}
- ✅ 機能の再利用を目的とする場合は has-a(委譲) を採用
- ✅ コンポーネント単位で機能を差し替えやすく、テストもしやすい
4. 継承 vs 委譲:判断基準
観点 | 継承(extends) | 委譲(composition) |
---|---|---|
関係性 | is-a | has-a |
構造の固定度 | 高い | 低い(差し替え可能) |
再利用性 | 限定的 | 高い |
拡張性 | サブクラスごとに増殖 | 必要部品だけ差し替え可能 |
テスト容易性 | 状態を持ちやすく重い | 単機能なので軽量 |
5. 抽象化とインタフェース設計の意識(TypeScriptとの連携)
interface Storable {
save(): void;
}
class FileStore implements Storable {
save() {
// ファイルへ保存
}
}
class DBStore implements Storable {
save() {
// DBへ保存
}
}
- ✅ 具象に依存せず、振る舞いの契約で設計できる
- ✅ 複数の実装を 構文レベルで切り替え可能
設計判断フロー
① クラスが担う責務は1つに明確化されているか?
② 継承の関係は is-a を満たすか? → それ以外なら委譲を検討
③ 再利用したいのは構造か振る舞いか? → 構造なら委譲が向く
④ テスト対象の振る舞いを独立させやすいか?
⑤ 構成要素はコンポーネントとして交換可能に設計されているか?
よくあるミスと対策
❌ 機能ごとにクラスを分けず、全てを1クラスに集約
→ ✅ 単一責任原則(SRP)を意識して分割
❌ 明確な is-a 関係がないのに extends を使っている
→ ✅ 機能の再利用は委譲で行い、構造の健全性を保つ
❌ クラスに直接Loggerや依存物を埋め込み、差し替えが困難
→ ✅ 外部から注入(DI)し、柔軟性を担保
結語
クラスは「便利な構文」ではない。
それは**“振る舞いと状態を構造として統合し、責任ある抽象化を行うための設計戦略”**である。
- 継承は拡張のために、委譲は柔軟性のために使う
- SRP・テスト容易性・構造の健全性をすべて意識する
- 機能単位で分離されたオブジェクト設計こそが、保守性を最大化する
JavaScriptにおけるクラス設計とは、
“責務と構造を秩序化し、進化に耐える抽象的インタフェースを構築する戦略である。”