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

Web APP JavaScript
スポンサーリンク

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

4日目のテーマは
「class を“アプリ全体の構造”にまで広げて、ドメイン(メモの世界)と UI(画面)をきれいに分けること」です。

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

  • 「Memo / MemoList」と「画面(DOM)」を直接くっつけない
  • 間に“司令塔”となるクラス(コントローラ)を置く
  • クラスごとの責任範囲を、アプリ全体の視点で整理する

ここをやっていきます。


まずは 3日目までの“ドメイン側”を軽く整理する

メモの世界(ドメイン)のクラスたち

ここまでで作ってきたのは、ざっくり言うとこんな構造です。

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

  toString() {
    const mark = this.#done ? "[x]" : "[ ]";
    return `#${this.#id} ${mark} ${this.#text}`;
  }
}

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

  findById(id) {
    return this.#memos.find(m => m.getId() === id) || null;
  }

  removeById(id) {
    const index = this.#memos.findIndex(m => m.getId() === id);
    if (index === -1) return false;
    this.#memos.splice(index, 1);
    return true;
  }
}
JavaScript

ここまでは「メモの世界」だけを考えていました。
ブラウザの画面(DOM)とは、まだつないでいません。

4日目では、この“ドメイン側”をそのままにして、
UI 側のクラスを新しく設計していきます。


ドメインと UI を分ける理由

もし DOM 操作を Memo に書き始めるとどうなるか

例えば、こう書き始めるとします。

class Memo {
  // 中略
  render(liElement) {
    liElement.textContent = this.toString();
  }
}
JavaScript

一見便利そうですが、
ここで Memo は「自分の状態」と「DOM のこと」の両方を知ることになります。

さらに進むと、

クリックされたら完了にする
ダブルクリックで編集モードにする

など、どんどん UI の都合が Memo の中に入ってきます。

結果として、

  • テストしづらい
  • Node.js などブラウザ以外の環境で使いづらい
  • UI を変えたいときに、ドメイン側まで書き換える必要が出る

という“べったり結合”状態になります。

責務分離の視点で言い直す

ドメイン(Memo / MemoList)の責務は、

「メモという概念を正しく扱うこと」
「ID・テキスト・完了状態・削除などのルールを守ること」

UI(画面)の責務は、

「ユーザーの入力を受け取ること」
「画面に状態を反映すること」

この2つを混ぜないために、
間に“橋渡し役”となるクラスを置きます。


UI 専用のクラスを作るという発想

役割をはっきり決める

ここで新しく、こんなクラスを考えます。

MemoAppController(名前は何でもいいですが、ここではこれでいきます)

このクラスの責務は、

  • MemoList を内部に持つ(ドメインの入り口)
  • DOM 要素(入力欄・追加ボタン・一覧表示エリア)を受け取る
  • イベント(クリック・入力)を受けて、MemoList を操作する
  • MemoList の状態を DOM に反映する

つまり、

「ドメイン」と「UI」をつなぐ“司令塔”

という立ち位置です。


MemoAppController の骨組みを作る

まずは constructor で“依存物”を受け取る

class MemoAppController {
  #memoList;
  #input;
  #addButton;
  #listContainer;

  constructor({ input, addButton, listContainer }) {
    this.#memoList = new MemoList();
    this.#input = input;
    this.#addButton = addButton;
    this.#listContainer = listContainer;

    this.#setupEvents();
    this.#render();
  }

  #setupEvents() {
    this.#addButton.addEventListener("click", () => {
      this.#handleAdd();
    });
  }

  #handleAdd() {
    const text = this.#input.value.trim();
    if (text === "") {
      alert("空のメモは追加できません");
      return;
    }
    this.#memoList.add(text);
    this.#input.value = "";
    this.#render();
  }

  #render() {
    // あとで実装
  }
}
JavaScript

ここでのカプセル化ポイントは、

  • DOM 要素(input / button / container)は private フィールドに持つ
  • イベント設定や描画処理は private メソッド(#setupEvents / #render)に閉じ込める
  • 外からは「new MemoAppController(…) するだけ」で動くようにする

という構造にしていることです。


#render で「ドメイン → UI」の反映を設計する

MemoList の状態を DOM に描画する

  #render() {
    this.#listContainer.innerHTML = "";

    const memos = this.#memoList.getAll();

    memos.forEach(memo => {
      const li = document.createElement("li");
      li.textContent = memo.toString();
      li.dataset.id = String(memo.getId());

      if (memo.isDone()) {
        li.classList.add("done");
      }

      li.addEventListener("click", () => {
        this.#toggleDone(memo.getId());
      });

      this.#listContainer.appendChild(li);
    });
  }

  #toggleDone(id) {
    const memo = this.#memoList.findById(id);
    if (!memo) return;

    if (memo.isDone()) {
      memo.markUndone();
    } else {
      memo.markDone();
    }

    this.#render();
  }
JavaScript

ここでの責務分離を整理すると、

MemoList は「メモの集まり」を管理するだけ。
Memo は「自分の状態」を管理するだけ。
MemoAppController は「DOM と MemoList の橋渡し」をする。

具体的には、

  • MemoList から getAll で Memo を受け取る
  • DOM 要素(li)を作って、Memo の情報を表示する
  • クリックイベントで Memo の ID を使って状態を変える
  • 状態が変わったら #render で画面を再描画する

という流れです。


カプセル化を「アプリ全体の入り口」にまで広げる

外から見たときに“触っていいのはどこか”

今の MemoAppController は、
外からはこう使うイメージです。

const input = document.querySelector("#memo-input");
const addButton = document.querySelector("#memo-add");
const listContainer = document.querySelector("#memo-list");

const app = new MemoAppController({
  input,
  addButton,
  listContainer
});
JavaScript

これでアプリは動き始めます。

外から見て触っているのは、

  • DOM 要素を取得する処理
  • MemoAppController の constructor

だけです。

MemoList の中身(#memos)
Memo の中身(#id / #text / #done)
MemoAppController の内部メソッド(#render / #toggleDone / #setupEvents)

これらはすべてカプセル化されていて、
外からは直接触れません。

重要な感覚:

「アプリ全体にも“外から触っていい入り口”と“中で閉じておくべき部分”がある」

ということです。


責務分離を“3層構造”で意識する

3つのレイヤーに分けて考える

ここまでの設計を、レイヤーで整理するとこうなります。

ドメイン層(ビジネスロジック)
Memo:1つのメモの状態と振る舞い。
MemoList:メモの集まりの管理(追加・検索・削除)。

アプリケーション層(司令塔)
MemoAppController:
ユーザー操作(クリックなど)を受けて、
MemoList を操作し、結果を UI に反映する。

UI 層(プレゼンテーション)
HTML / CSS:
input / button / ul などの見た目と配置。

この3つを意識しておくと、

  • UI を変えたいとき → HTML / CSS / Controller だけ触ればいい
  • メモのルールを変えたいとき → Memo / MemoList だけ触ればいい

という“変更の影響範囲”が小さくなります。


小さな例題としての「削除ボタン」追加

責務を崩さずに機能を足してみる

例えば、「削除ボタン」を付けたいとします。

やるべきことはこうです。

  • UI に「削除」ボタンを追加する(li の中に)
  • クリックされたら、その Memo の ID を使って MemoList.removeById を呼ぶ
  • そのあと #render で画面を更新する

コードとしては、#render を少し変えるだけで済みます。

  #render() {
    this.#listContainer.innerHTML = "";

    const memos = this.#memoList.getAll();

    memos.forEach(memo => {
      const li = document.createElement("li");

      const textSpan = document.createElement("span");
      textSpan.textContent = memo.toString();
      li.appendChild(textSpan);

      const deleteButton = document.createElement("button");
      deleteButton.textContent = "削除";
      deleteButton.addEventListener("click", (event) => {
        event.stopPropagation(); // li のクリックとバッティングしないように
        this.#deleteMemo(memo.getId());
      });
      li.appendChild(deleteButton);

      li.addEventListener("click", () => {
        this.#toggleDone(memo.getId());
      });

      this.#listContainer.appendChild(li);
    });
  }

  #deleteMemo(id) {
    this.#memoList.removeById(id);
    this.#render();
  }
JavaScript

ここでも、

削除の実体(配列から消す)は MemoList の責任。
どの ID を消すか決めるのは Controller の責任。
ボタンの見た目は UI(HTML / CSS)の責任。

という分担が崩れていません。


4日目のまとめ:class を“アプリの骨組み”として見る

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

「class を“1個のオブジェクト”ではなく、“アプリ全体のレイヤー構造”として設計する」

です。

Memo:1つのメモのルールを守る。
MemoList:メモの集まりのルールを守る。
MemoAppController:ドメインと UI をつなぐ司令塔。

それぞれが、

自分の責任範囲だけを引き受け、
中身はカプセル化し、
外には必要な窓口だけを公開する。

ここまで意識できていれば、
5日目以降で「バリデーションを強化する」「永続化(localStorage)を足す」
といった現実寄りの要素を追加しても、
設計が崩れずに育てていけます。

もし余裕があれば、

  • 完了したメモだけを別エリアに表示する
  • 検索ボックスを追加して、キーワードで絞り込む

などを、自分なりに
「どのクラスの責任か?」を考えながら足してみてください。
それが、オブジェクト指向の設計センスを一段引き上げてくれます。

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