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

Web APP JavaScript
スポンサーリンク

2日目のゴールと今日やること

2日目のテーマは
「class の“中身の振る舞い”をちゃんと設計して、オブジェクトを“ただのデータ”から“自分で動ける存在”にすること」です。

キーワードは昨日と同じく
class/カプセル化/責務分離
ですが、今日は特に

「メソッドの設計」
「状態をどう変えるか」
「“どこに書くべき処理か”を見極める」

ここを一段深くやっていきます。


まずは 1日目の Memo / MemoList を少し思い出す

シンプルな Memo と MemoList のおさらい

昨日のイメージはこんな感じでした。

class Memo {
  #text;

  constructor(text) {
    this.#text = text;
  }

  getText() {
    return this.#text;
  }

  setText(newText) {
    if (typeof newText !== "string") {
      console.warn("文字列以外はセットできません");
      return;
    }
    this.#text = newText;
  }
}

class MemoList {
  #memos;

  constructor() {
    this.#memos = [];
  }

  add(text) {
    const memo = new Memo(text);
    this.#memos.push(memo);
  }

  getAll() {
    return this.#memos;
  }
}
JavaScript

ここまでは、

Memo は「1つのメモ」を表す。
MemoList は「メモの集まり」を管理する。

という“役割の分け方”を体感してもらいました。

今日はここに「振る舞い(メソッド)」を足していきます。


「ただのデータ入れ」から「自分で動けるオブジェクト」へ

データだけ持つオブジェクトは、まだ“構造体”レベル

例えば、こういうのは「ただのデータ入れ」です。

const memo = {
  text: "牛乳を買う",
  done: false
};
JavaScript

これはこれで使えますが、
「完了にする」「未完了に戻す」などの“振る舞い”は
外側の関数で書くことになります。

function toggleDone(m) {
  m.done = !m.done;
}
JavaScript

これをクラスでやると、
「メモ自身が自分の状態を変える」ように書けます。


状態と振る舞いをセットで持つ Memo クラス

完了フラグを持たせてみる

まずは Memo に「完了フラグ」を足します。

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;
  }
}
JavaScript

ここでのポイントは、

完了しているかどうか(#done)は外から直接触れない。
代わりに、isDone / markDone / markUndone という“振る舞い”を用意する。

つまり、

「状態(#done)と、それをどう変えるか(メソッド)をセットでクラスに閉じ込める」

これがオブジェクト指向っぽさの一歩目です。


カプセル化を“状態+振る舞い”の単位で考える

なぜ #done を外から触らせないのか

もし #done を public にしてしまうと、
どこからでもこう書けてしまいます。

memo.done = "はい"; // 真偽値じゃないものを入れてしまえる
JavaScript

これを許すと、

「done は true / false のはずなのに、どこかで文字列が入ってバグる」

という未来が待っています。

カプセル化の本質は、

「そのクラスが“こうあってほしい”というルールを、
外から壊されないようにする」

ことです。

Memo の場合、

text は文字列であってほしい。
done は true / false であってほしい。

だから、

text は setText の中でチェックする。
done は markDone / markUndone だけが変える。

という設計にします。


責務分離を“メソッドレベル”で見る

どの処理を Memo に書くべきか?

例えば、こんな処理を考えます。

「完了しているメモには [x]、
未完了のメモには [ ] を付けて表示したい」

これをどこに書くべきか?という話です。

パターンA:外側の関数でやる。

function printMemo(memo) {
  const mark = memo.isDone() ? "[x]" : "[ ]";
  console.log(mark, memo.getText());
}
JavaScript

パターンB:Memo クラスに「自分を表示する」メソッドを持たせる。

class Memo {
  // さっきの定義に追加
  print() {
    const mark = this.#done ? "[x]" : "[ ]";
    console.log(mark, this.#text);
  }
}
JavaScript

どちらもアリですが、
オブジェクト指向の考え方に寄せるなら、

「自分の表示の仕方は、自分が知っている」

と考えて Memo に print() を持たせるのは、かなり自然です。

責務分離をメソッドレベルで言い換えると、

「この処理は“誰の仕事”か?」

を常に自問することです。

完了フラグの扱い → Memo の仕事。
メモの集まりの検索 → MemoList の仕事。
画面(DOM)への描画 → 画面担当のクラス or 関数の仕事。

こうやって“仕事の担当者”を決めていきます。


MemoList に「振る舞い」を足していく

完了しているメモだけを取り出す

MemoList にも、
「集まりとしての振る舞い」を足していきます。

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(memo => memo.isDone());
  }

  getUndoneMemos() {
    return this.#memos.filter(memo => !memo.isDone());
  }
}
JavaScript

ここでの責務分離はこうです。

「完了しているかどうか」は Memo が知っている(isDone)。
「完了しているメモだけを集める」のは MemoList の仕事。

MemoList は「集まりとしての操作」を担当し、
個々の Memo の中身には踏み込みすぎないようにします。


小さなシナリオで“責務の流れ”を追ってみる

実際に動かしてみるコード

const list = new MemoList();

list.add("牛乳を買う");
list.add("本を読む");
list.add("メールを返す");

const all = list.getAll();
all[1].markDone(); // 2番目のメモを完了にする

console.log("=== すべてのメモ ===");
all.forEach(m => m.print());

console.log("=== 完了したメモ ===");
list.getDoneMemos().forEach(m => m.print());

console.log("=== 未完了のメモ ===");
list.getUndoneMemos().forEach(m => m.print());
JavaScript

ここで起きていることを、責務の流れで見るとこうです。

MemoList は「メモの集まり」を持っている。
Memo は「自分の text と done 状態」を持っている。
完了にするのは Memo の仕事(markDone)。
完了しているメモだけ集めるのは MemoList の仕事(getDoneMemos)。
表示の仕方は Memo の仕事(print)。

誰が何を担当しているかが、
かなりハッキリしてきています。


カプセル化を“守りたいルール”から逆算する

ルールを日本語で書いてから、class に落とす

2日目で一番深掘りしてほしいのはここです。

クラスを設計するときは、
まず「守りたいルール」を日本語で書き出す。

例えば Memo なら、

text は必ず文字列。
done は true / false だけ。
外から勝手に done を書き換えられたくない。
表示の形式は Memo の中で決めたい。

このルールを守るために、

#text / #done を private にする。
setText で型チェックをする。
markDone / markUndone だけが done を変える。
print メソッドで表示形式を統一する。

というふうに、
カプセル化とメソッド設計が“ルールから逆算されている”状態が理想です。


2日目のまとめ:class を「箱」ではなく「役割」として見る

今日の本質を一言で言うと、

「class は“データの箱”ではなく、“状態と振る舞いをまとめた役割”だ」

ということです。

Memo は「1つのメモ」という役割。
MemoList は「メモの集まりを管理する」という役割。

それぞれが、

自分の状態をカプセル化し、
自分の責任範囲の処理だけを引き受ける。

この感覚が持てていれば、
3日目以降で「画面(DOM)とクラスをつなぐ」「イベントとクラスを組み合わせる」
といった、より“アプリっぽい”世界に進んでも、
クラス設計で迷子になりにくくなります。

もし余裕があれば、

Memo に「重要フラグ(priority)」を足してみる
MemoList に「重要なメモだけを返すメソッド」を足してみる

など、自分なりに“ルールを決めて → カプセル化して → 責務を分ける”
という流れを遊んでみてください。
そこから先は、もう立派なクラス設計の練習です。

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