横配置インスタンス指向 ― JS 時代の「継承しない」クラス設計
🎯 はじめに
JavaScript の世界では、継承中心の古典的 OOP (オブジェクト指向) は使いづらいです。
本記事では、JS の特性を活かした 横配置インスタンス指向(Composition First) を中心に、
- なぜ継承が破綻するのか
- なぜ JS は横配置と相性が良いのか
- シャロー/ディープコピー
- コンストラクタと clone / 静的ファクトリの扱い
をまとめます。
本記事ではこの内容を 横配置インスタンス指向 と呼ぶことにします。
🧩 横配置インスタンス指向とは?
- インスタンス(実体)の状態を中心に設計する。
- 継承階層は浅く保ち、縦方向の複雑化を避ける。
- 複数インスタンスを横に並べて相互運用する。
- データ(状態)と振る舞い(ロジック)を分離する。
- 機能は差し替え可能な部品(コンポーネント)として扱う。
- ロジックは関数として外部化し、インスタンスは状態の容器とする。
- コピーが容易な浅い構造を保つ(Shallow-Friendly Design)。
- 縦に積むのではなく横に並べる ― 直列ではなく並列の設計思想。
本記事はMFC(Microsoft Foundation Class)で作者が散々苦労した点と.NETで解放された点のまとめということです
🧩 JS における「縦配置(継承)」と「横配置(インスタンス)」
🟥 縦配置:継承中心(古典的 OOP)
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
問題点
- 階層が縦に伸びるほど保守不能
- 未来の完全設計が必要
- 密結合
- 差し替え困難
- JS の柔軟性と相性が悪い
🟩 横配置:インスタンス中心(Composition First)
const position = { x: 0, y: 0 };
const velocity = { vx: 1, vy: 2 };
const entity = {
position,
velocity,
renderer: createRenderer(),
collider: createCollider(),
};
利点
- 必要な機能だけを横に並べる
- 差し替えが容易
- テストしやすい
- JS の「オブジェクトをデータ構造として扱う」スタイルと相性が良い
- JS エコシステムでは、React / Vue / Node.js をはじめとして
コンポーネント化・関数化・オブジェクトの組み合わせが一般的で、
横配置インスタンス指向と非常に相性が良い設計になっています。
もちろん、継承には継承の良さ便利さもあるのですが
保守性・拡張性の困難さが最大のネックになります。
🧠 JS は「インスタンス指向」と相性が良すぎる理由
✔ オブジェクトがただの辞書
const obj = { a: 1, b: 2 };
obj.c = 3; // 後付けで機能追加できる
✔ 関数が一級市民
function move(entity, dt) {
entity.position.x += entity.velocity.vx * dt;
}
✔ class が「構造体の拡張」に近い
class Entity {
constructor(pos, vel) {
this.position = pos;
this.velocity = vel;
}
}
✔ 継承より mixin / 関数注入が自然
const withRenderer = (entity) => ({
...entity,
renderer: createRenderer(),
});
JS は「横に並べる」設計が自然にできる言語。
次項は長いので深堀りとしての説明であるので「横配置インスタンス指向の基本構造」まで読み飛ばしても構いません
⚠️ スプレッド構文は prototype をコピーしません
...entityは「データだけ」をコピーし、メソッドは消えます- つまり 新しいオブジェクトは元の class のインスタンスではなくなる
- 対策:メソッドは prototype に置く(データはスプレッドで横に拡張)
// 1. コンストラクタ(データ構造の初期化)
class Entity {
constructor(position, velocity) {
this.position = position;
this.velocity = velocity;
}
}
// 2. プロトタイプにメソッドを直接拡張する(データを直接操作・計算する処理など)
Entity.prototype.integrate = function (dt) {
this.position.x += this.velocity.vx * dt;
this.position.y += this.velocity.vy * dt;
};
// 使い方
const body = new Entity({ x: 0, y: 0 }, { vx: 1, vy: 2 });
// スプレッド構文でデータを拡張して新しいオブジェクトを作っても……
const extendedBody = {
...body,
extraFlag: true
};
// 元のインスタンス(body)はプロトタイプ経由でメソッドを呼び出せる
body.integrate(1);
-
要するにどういうこと?
「データ(中身)と振る舞い(動き)の居場所をきっちり分ける」ということです。
スプレッド構文は、オブジェクトを新しくコピーしたり、別のプロパティをちょい足しして形を変えたりする「データの組替え・拡張」には抜群に便利です。しかし、そのやり方だけに頼ると、オブジェクトに紐づいていたメソッド(関数)まで一緒に溶けて消えてしまいます。
そこで、「変化しやすいデータ」はスプレッドで自由に横へ並べて拡張しつつ、「変化しない共通のメソッド(計算処理など)」は prototype という安全な母港にしっかりと定住させておく。
この役割分担を守ることで、JSの柔軟なデータ操作(横配置のメリット)を手放すことなく、メソッドの消滅という思わぬ罠を綺麗に回避できるようになります。
🧩 横配置インスタンス指向の基本構造(JS版)
1. データは構造体(ただのオブジェクト)
const Position = (x, y) => ({ x, y });
const Velocity = (vx, vy) => ({ vx, vy });
2. 振る舞いは関数として渡す
const integrate = (entity, dt) => {
entity.position.x += entity.velocity.vx * dt;
entity.position.y += entity.velocity.vy * dt;
};
3. クラスは「構造体の束」+「関数の束」
class Entity {
constructor(position, velocity) {
this.position = position;
this.velocity = velocity;
}
}
4. 機能追加は“横に並べる”だけ
const addCollider = (entity) => ({
...entity,
collider: createCollider(),
});
継承不要。
🔥 JS におけるコピー問題
JS はコピーの挙動が独特なので、横配置設計では特に重要。
インスタンス管理が問題になることがある。
🟦 シャローコピー(浅いコピー)
const a = { x: 1, y: { z: 2 } };
const b = { ...a };
b.y.z = 999; // a.y.z も 999 になる(共有)
利点
- 速い
- 構造体が浅い場合は安全
欠点
- 深い構造では「意図せず共有」バグが起きる
🟩 ディープコピー(深いコピー)
const deep = JSON.parse(JSON.stringify(a));
あるいは
const deep = structuredClone(a);
利点
- 比較的に安全
欠点
- 比較的重い場合がある
- 関数やDOMノードなどはコピーできない(structuredCloneの場合)
- JSON方式の場合は Date / Map / Set / 関数などが壊れる
🟨 コンストラクタ(初期化)
class Entity {
constructor(position = { x: 0, y: 0 }, velocity = { x: 0, y: 0 }) {
this.position = { ...position };
this.velocity = { ...velocity };
}
// コピー用
clone() {
return new Entity(this.position, this.velocity);
}
// または静的ファクトリ
static from(src) {
return new Entity(src.position, src.velocity);
}
}
🟩 クローン
// コピー用
clone() {
return new Entity(this);
}
clone() は、そのクラスが定義する「複製単位」に応じてコピー方法を選択する
🟧 多用途コンストラクタ(多目的初期化)
class Entity {
constructor(positionOrSrc, velocity) {
// コピーの場合
if (positionOrSrc instanceof Entity) {
this.position = { ...positionOrSrc.position };
this.velocity = { ...positionOrSrc.velocity };
}
// 新規作成の場合
else {
this.position = { ...(positionOrSrc || { x: 0, y: 0 }) };
this.velocity = { ...(velocity || { x: 0, y: 0 }) };
}
}
// コピー用
clone() {
return new Entity(this);
}
}
複雑さが増しますがクラス利用の記述を簡単にしたい場合などに使います。
注意点:
- instanceof が予期せず false を返すトラブルの可能性
- 例えば「異なるウィンドウやiframeをまたいだ場合、またはシリアライズ後に復元した場合など」
🧠 横配置でコピーが簡単になる理由
- JS の構造体はただのオブジェクト
- 継承階層がないので複製対象を明示しやすい
- MFC(Microsoft Foundation Class) のような複雑な継承構造では、基底クラスから派生クラスまで複製仕様を意識する必要があり、複製が複雑になりやすい
- 必要な部分だけコピーすればよい
🎯 まとめ
- JS は継承より横配置インスタンス指向と相性が良い
- class はただのデータ構造をまとめる用途で使うのが、保守性・拡張性の面で都合がいい
- 機能はインスタンスを横に並べて組み合わせる(コンポーネント方式)
- 関数を外部化できる JS の強みなのでメソッドは単一機能の関数の寄せ集めとして作る
- コピー問題も横配置なら単純
- TypeScriptでは機能単位の型を組み合わせる設計に発展させやすい
- 継承中心の OOP は JS では使いづらい
- 少数のエリート完璧巨人を作るのではなく多数の専門職の小人村を作る
- つまり、継承階層を縦に積み上げるという直列配置ではなく独立した機能を横方向に組み合わせるという並列配置を心掛ける
作者
GitHub: https://github.com/NAS6mixfoolv
X(旧Twitter): https://x.com/NAS6_oxo
作者HP: https://nas6.net
気に入っていただけたら GitHub に ⭐ をいただけると嬉しいです!