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)を足す」
といった現実寄りの要素を追加しても、
設計が崩れずに育てていけます。
もし余裕があれば、
- 完了したメモだけを別エリアに表示する
- 検索ボックスを追加して、キーワードで絞り込む
などを、自分なりに
「どのクラスの責任か?」を考えながら足してみてください。
それが、オブジェクト指向の設計センスを一段引き上げてくれます。


