JavaScript | 1 日 120 分 × 7 日アプリ学習:クラス設計アプリ(オブジェクト指向)

Web APP JavaScript
スポンサーリンク

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 に「更新日時の新しい順に並べる」メソッドを足す

など、自分なりに「ルールを決めて → カプセル化して → 責務を分ける」
という流れをもう一度やってみてください。
それが、オブジェクト指向の筋トレそのものです。

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