5日目のゴールと今日やること
5日目のテーマは
「class を“長く動き続けるアプリ”として育てるために、バリデーションと永続化を設計すること」です。
キーワードはいつも通り
class/カプセル化/責務分離。
今日は特に、
class の中に「入力チェック(バリデーション)」をどう組み込むか
localStorage を使った「保存・読み込み」をどのクラスに持たせるか
「ドメインのルール」と「保存形式」をどう分けるか
ここを深掘りしていきます。
ここまでの構造を一度“俯瞰”しておく
ドメイン層とコントローラ層の整理
4日目までで、ざっくりこんな構造ができていました。
Memo
1つのメモの状態(id, text, done)と、その振る舞い(完了・未完了・表示など)を担当する。
MemoList
メモの集まりを管理する(追加・検索・削除・ID 発行など)。
MemoAppController
DOM(input, button, list)と MemoList をつなぐ。
イベントを受けて MemoList を操作し、結果を画面に反映する。
今日はここに「バリデーション」と「永続化(localStorage)」を足しますが、
大事なのは「どの責任をどのクラスに持たせるか」を崩さないことです。
バリデーションを“どこに書くか”問題
まずはルールを日本語で決める
メモの text に対して、例えばこんなルールを決めます。
空文字はダメ
50文字を超えたらダメ
スペースだけの文字列もダメ
このときに考えるべきは、
「このルールは、どのクラスの責任か?」
ということです。
メモの内容そのものに関するルールなので、
本質的には Memo の責任です。
ただし、「ユーザーにどう伝えるか」(alert を出すなど)は
UI 側の責任です。
ここを分けて考えます。
Memo に「バリデーションのルール」を閉じ込める
ルールだけを返すメソッドを用意する
Memo クラスに、
「この text は有効かどうか」を判定する静的メソッドを追加します。
class Memo {
static validateText(text) {
if (typeof text !== "string") {
return { ok: false, message: "文字列を入力してください" };
}
const trimmed = text.trim();
if (trimmed === "") {
return { ok: false, message: "空のメモは追加できません" };
}
if (trimmed.length > 50) {
return { ok: false, message: "50文字以内で入力してください" };
}
return { ok: true, message: "" };
}
#id;
#text;
#done;
constructor(id, text) {
const result = Memo.validateText(text);
if (!result.ok) {
throw new Error("不正なメモの内容です: " + result.message);
}
this.#id = id;
this.#text = text.trim();
this.#done = false;
}
setText(newText) {
const result = Memo.validateText(newText);
if (!result.ok) {
console.warn("メモの更新に失敗:", result.message);
return false;
}
this.#text = newText.trim();
return true;
}
// それ以外は前回と同じ
}
JavaScriptここでのカプセル化のポイントは、
text に関するルールは Memo の中に閉じ込める
外からは「validateText の結果」だけを見ればよい
constructor / setText の中でも同じルールを使い回せる
という構造になっていることです。
UI 側は「ルールの中身」を知らなくても、
Memo.validateText(text) を呼んで
ok / message を見て判断するだけで済みます。
UI 側では「ルールを使うだけ」に徹する
Controller から見たバリデーション
MemoAppController の追加処理を、
バリデーション対応に書き換えます。
class MemoAppController {
// フィールドや constructor は省略
#handleAdd() {
const raw = this.#input.value;
const result = Memo.validateText(raw);
if (!result.ok) {
alert(result.message);
return;
}
this.#memoList.add(raw);
this.#input.value = "";
this.#render();
}
}
JavaScriptここでの責務分離はこうです。
「何が有効な text か」は Memo の責任。
「エラーをどうユーザーに伝えるか」は Controller(UI)の責任。
Controller は Memo のルールを“利用する側”であって、
ルールそのものは知らなくていい。
これが、
「バリデーションのロジックをドメイン側に、
エラーメッセージの見せ方を UI 側に分ける」
という設計です。
永続化(localStorage)をどこに持たせるか
いきなり MemoList に書き始めると危険な理由
「メモをブラウザに保存したい」となったとき、
真っ先にこう書きたくなるかもしれません。
class MemoList {
save() {
localStorage.setItem("memos", JSON.stringify(this.#memos));
}
}
JavaScript一見シンプルですが、
ここで MemoList は「ブラウザの localStorage の存在」を知ってしまいます。
もし将来、
サーバーに保存したくなった
別のストレージ方式に変えたくなった
ときに、MemoList を書き換える必要が出てきます。
MemoList の本来の責任は
「メモの集まりを管理すること」であって、
「どこに保存するか」ではありません。
そこで、永続化専用のクラスを用意します。
Storage 専用クラスを作るという発想
MemoStorage の役割を決める
新しく MemoStorage というクラスを作ります。
このクラスの責任は、
MemoList の状態を保存する
保存された状態から MemoList を復元する
だけです。
どこに保存するか(localStorage なのか、他なのか)は
このクラスの中に閉じ込めます。
MemoStorage の実装例
class MemoStorage {
static KEY = "memo-app-data";
static save(memoList) {
const data = memoList.getAll().map(memo => ({
id: memo.getId(),
text: memo.getText(),
done: memo.isDone()
}));
const json = JSON.stringify(data);
localStorage.setItem(MemoStorage.KEY, json);
}
static load() {
const json = localStorage.getItem(MemoStorage.KEY);
if (!json) {
return new MemoList();
}
let rawArray;
try {
rawArray = JSON.parse(json);
} catch (e) {
console.warn("保存データの読み込みに失敗しました", e);
return new MemoList();
}
const list = new MemoList();
let maxId = 0;
rawArray.forEach(item => {
const memo = list.add(item.text);
if (item.done) {
memo.markDone();
}
if (memo.getId() > maxId) {
maxId = memo.getId();
}
});
return list;
}
}
JavaScriptここでのカプセル化・責務分離を整理すると、
MemoStorage は localStorage を知っているが、
Memo / MemoList は localStorage を知らない。
MemoList は「自分の中身をどう保存するか」は知らないが、
getAll で「今の状態」を外に渡すことはできる。
保存形式(JSON の形)は MemoStorage の中に閉じ込める。
という構造になっています。
Controller から見た「保存」と「読み込み」
アプリ起動時に読み込む
MemoAppController の constructor を、
MemoStorage を使う形に変えます。
class MemoAppController {
#memoList;
#input;
#addButton;
#listContainer;
constructor({ input, addButton, listContainer }) {
this.#memoList = MemoStorage.load();
this.#input = input;
this.#addButton = addButton;
this.#listContainer = listContainer;
this.#setupEvents();
this.#render();
}
// 省略
}
JavaScript状態が変わったタイミングで保存する
追加・完了切り替え・削除など、
「メモの状態が変わる」タイミングで save を呼びます。
#handleAdd() {
const raw = this.#input.value;
const result = Memo.validateText(raw);
if (!result.ok) {
alert(result.message);
return;
}
this.#memoList.add(raw);
this.#input.value = "";
MemoStorage.save(this.#memoList);
this.#render();
}
#toggleDone(id) {
const memo = this.#memoList.findById(id);
if (!memo) return;
if (memo.isDone()) {
memo.markUndone();
} else {
memo.markDone();
}
MemoStorage.save(this.#memoList);
this.#render();
}
#deleteMemo(id) {
this.#memoList.removeById(id);
MemoStorage.save(this.#memoList);
this.#render();
}
JavaScriptここでの責務分離はこうです。
「いつ保存するか」を決めるのは Controller の責任。
「どう保存するか」を決めるのは MemoStorage の責任。
「何を保存するか」(メモの状態)は Memo / MemoList の責任。
3者がそれぞれの役割を持ちつつ、
お互いの中身には踏み込みすぎないようになっています。
5日目でいちばん深く理解してほしいこと
今日の本質を、あえて日本語だけでまとめます。
バリデーションのルールは「ドメインの一部」。
だから Memo に閉じ込めて、
UI はその結果(ok / message)だけを使う。
永続化は「どこに・どう保存するか」という技術的な都合。
だから MemoStorage のような専用クラスに閉じ込めて、
Memo / MemoList は保存先を知らないまま動けるようにする。
Controller は「いつルールを使うか」「いつ保存するか」を決める司令塔。
UI のイベントとドメインの操作をつなぐのが仕事。
そして、どのクラスも
自分の責任範囲だけを引き受ける
中身はカプセル化して守る
外には必要な窓口(メソッド)だけを公開する
という姿勢を崩さない。
ここまで意識できていれば、
このメモアプリはもう「練習用」ではなく、
普通に毎日使えるレベルの設計になっています。
もし余裕があれば、
完了したメモだけを保存しない設定を試してみる
バリデーションルールを変えてみる(100文字までにするなど)
保存キー(MemoStorage.KEY)を変えて複数アプリを共存させる
といった“設計の微調整”を、自分の手でやってみてください。
その試行錯誤こそが、オブジェクト指向のセンスを一段深くしてくれます。


