3日目のゴールと今日やること
3日目のテーマは
「class を“現実のアプリ構造”に近づけるために、ID・検索・更新・削除を設計すること」です。
キーワードは同じく
class/カプセル化/責務分離
ですが、今日は特に
- 「1つのオブジェクトを一意に識別する ID」
- 「検索・更新・削除の責任をどこに持たせるか」
- 「クラス同士の関係をどう整理するか」
ここをじっくりやります。
まずは 2日目までの Memo / MemoList を少し整理する
今の時点のイメージ
ここまでのイメージは、だいたいこんな感じでした。
class Memo {
#text;
#done;
constructor(text) {
this.#text = text;
this.#done = false;
}
getText() {
return this.#text;
}
setText(newText) {
if (typeof newText !== "string") {
console.warn("文字列以外はセットできません");
return;
}
this.#text = newText;
}
isDone() {
return this.#done;
}
markDone() {
this.#done = true;
}
markUndone() {
this.#done = false;
}
print() {
const mark = this.#done ? "[x]" : "[ ]";
console.log(mark, this.#text);
}
}
class MemoList {
#memos;
constructor() {
this.#memos = [];
}
add(text) {
const memo = new Memo(text);
this.#memos.push(memo);
}
getAll() {
return this.#memos;
}
getDoneMemos() {
return this.#memos.filter(m => m.isDone());
}
getUndoneMemos() {
return this.#memos.filter(m => !m.isDone());
}
}
JavaScriptここまでは、
Memo は「1つのメモ」
MemoList は「メモの集まり」
という役割分担でした。
3日目では、ここに「ID」「検索」「削除」を足していきます。
なぜ ID が必要になるのか
「何番のメモを消したいか」をどう指定する?
現実のアプリでは、こういう操作をしたくなります。
- 「このメモだけ削除したい」
- 「このメモだけ内容を変更したい」
そのときに「配列のインデックス(0番、1番…)」で指定していると、
並び替えや削除のたびにズレてしまいます。
そこで出てくるのが「ID」です。
ID は「そのオブジェクトを一意に識別するラベル」
ID は、ざっくり言うと
「このメモは、この世界でこの1つだけ、という印」
です。
例えば、
id: 1 のメモ
id: 2 のメモ
id: 3 のメモ
JavaScriptというふうに番号を振っておけば、
「id が 2 のメモを削除したい」
「id が 3 のメモの内容を変えたい」
といった操作がやりやすくなります。
Memo に ID を持たせる設計
ID をどこで決めるか
ID を決める場所は、2つ候補があります。
- Memo 自身が「自分の ID」を決める
- MemoList が「新しい Memo に ID を割り当てる」
今回は「集まりを管理する側(MemoList)が ID を配る」設計にします。
理由はシンプルで、
「ID の重複が起きないように管理するのは、集まりの責任だから」です。
Memo に id フィールドを追加する
class Memo {
#id;
#text;
#done;
constructor(id, text) {
this.#id = id;
this.#text = text;
this.#done = false;
}
getId() {
return this.#id;
}
getText() {
return this.#text;
}
setText(newText) {
if (typeof newText !== "string") {
console.warn("文字列以外はセットできません");
return;
}
this.#text = newText;
}
isDone() {
return this.#done;
}
markDone() {
this.#done = true;
}
markUndone() {
this.#done = false;
}
print() {
const mark = this.#done ? "[x]" : "[ ]";
console.log(`#${this.#id}`, mark, this.#text);
}
}
JavaScriptここでのカプセル化ポイントは、
id も private(#id)にして、外から勝手に変えられないようにする。
外からは getId() で読むだけにする。
ID は「一度決めたら変えない」前提なので、
setter(setId)はあえて用意しません。
MemoList に ID 発行と検索・削除を持たせる
ID を配るカウンタを持つ
MemoList に「次に使う ID」を持たせます。
class MemoList {
#memos;
#nextId;
constructor() {
this.#memos = [];
this.#nextId = 1;
}
add(text) {
const memo = new Memo(this.#nextId, text);
this.#memos.push(memo);
this.#nextId += 1;
return memo;
}
getAll() {
return this.#memos;
}
}
JavaScriptここでの責務分離は、
ID の重複が起きないようにする → MemoList の仕事。
ID を持っておく → Memo の仕事。
という分担になっています。
ID で検索するメソッド
findById(id) {
return this.#memos.find(memo => memo.getId() === id) || null;
}
JavaScriptここでのカプセル化ポイントは、
MemoList は Memo の内部構造(#id)には触らず、
getId() という「窓口メソッド」だけを使っていることです。
ID で削除するメソッド
removeById(id) {
const index = this.#memos.findIndex(memo => memo.getId() === id);
if (index === -1) {
return false;
}
this.#memos.splice(index, 1);
return true;
}
JavaScriptここでも、
「どの Memo を削除するか」は ID で決める。
「配列から削除する」のは MemoList の仕事。
という責務分離になっています。
小さなシナリオで ID・検索・削除を体験する
実際に動かしてみるコード
const list = new MemoList();
const m1 = list.add("牛乳を買う");
const m2 = list.add("本を読む");
const m3 = list.add("メールを返す");
console.log("=== 追加直後 ===");
list.getAll().forEach(m => m.print());
console.log("id 2 のメモを完了にする");
const target = list.findById(2);
if (target) {
target.markDone();
}
console.log("=== 完了状態を反映 ===");
list.getAll().forEach(m => m.print());
console.log("id 1 のメモを削除する");
const removed = list.removeById(1);
console.log("削除成功?", removed);
console.log("=== 削除後 ===");
list.getAll().forEach(m => m.print());
JavaScriptここでの流れを、責務の観点で追ってみます。
MemoList は add で ID を配りながら Memo を作る。
Memo は自分の id / text / done を持つ。
findById は「ID から Memo を探す」責任を持つ。
removeById は「ID から Memo を削除する」責任を持つ。
完了状態の変更(markDone)は Memo の責任。
誰が何を担当しているかが、かなりクリアです。
カプセル化を「クラス間の関係」にまで広げて考える
「直接触る」のはどこまで許すか
例えば、こういう書き方もできます。
const all = list.getAll();
all[0].markDone();
JavaScriptこれは「MemoList の外から、Memo のメソッドを呼んでいる」状態です。
これはアリです。なぜなら、
Memo の public メソッド(markDone)は
「外から呼ばれてもいい窓口」だからです。
一方で、こういうのは NG にしたい。
all[0].#done = true; // そもそも構文エラーになる
JavaScriptこれは「カプセル化で守っている中身を、外から壊そうとしている」行為です。
カプセル化の本質は、
「外から触っていいところ」と「触ってほしくないところ」を、
クラスの設計でハッキリ分けることです。
Memo の場合、
触っていいのは getId / getText / setText / isDone / markDone / markUndone / print。
触ってほしくないのは #id / #text / #done。
MemoList の場合、
触っていいのは add / getAll / findById / removeById。
触ってほしくないのは #memos / #nextId。
この線引きが、クラス設計の“防御力”を決めます。
責務分離を「クラス同士の関係」で再確認する
どこまでを MemoList にやらせるか
例えば、こんなメソッドを MemoList に足すこともできます。
printAll() {
this.#memos.forEach(memo => memo.print());
}
JavaScriptこれは、
「一覧表示の責任を MemoList に持たせる」設計です。
一方で、
「表示は UI 担当のクラスや関数に任せる」
「MemoList は“データ管理”だけに集中させる」
という設計もあります。
どちらが正解、ではなく、
「このクラスにはどこまでの責任を持たせるか?」
を意識して決めることが大事です。
個人的な指針としては、
Memo:1つのメモの状態と、その状態に関する振る舞い。
MemoList:メモの集まりの管理(追加・検索・削除)。
UI(画面):表示やユーザー入力の処理。
という三層に分けると、かなりスッキリします。
3日目では、まだ UI は軽く触れる程度で、
Memo / MemoList の責務を固めるところまでで十分です。
3日目のまとめ:ID・検索・削除で見える「設計の筋肉」
今日の本質を一言でまとめると、
「class 同士の関係を意識して、ID・検索・削除を“誰の責任か”で分けて設計する」
です。
ID を配るのは集まり側(MemoList)の責任。
ID を持っておくのは個体側(Memo)の責任。
検索・削除は「集まりとしての操作」なので MemoList の責任。
完了状態の変更は Memo の責任。
そして、
中身(#フィールド)はカプセル化で守り、
外から触っていい窓口(メソッド)だけを公開する。
ここまで意識できていれば、
4日目以降で「DOM とクラスをつなぐ」「イベントからクラスを呼ぶ」
といった“アプリっぽい世界”に入っても、
クラス設計の軸がブレにくくなります。
もし余裕があれば、
- Memo に「作成日時」「更新日時」を足す
- MemoList に「更新日時の新しい順に並べる」メソッドを足す
など、自分なりに「ルールを決めて → カプセル化して → 責務を分ける」
という流れをもう一度やってみてください。
それが、オブジェクト指向の筋トレそのものです。


