6日目のゴールと今日やること
6日目のテーマは
「class を“拡張しやすい設計”に育てるために、状態遷移とエラー処理をちゃんとクラス側でデザインすること」です。
キーワードはいつも通り
class/カプセル化/責務分離。
今日は特に、
- 「状態が増えたときにクラス設計が耐えられるか」
- 「エラーや“ありえない状態”をどこで止めるか」
- 「UI の都合でドメインを汚さない」
ここを一段深く見ていきます。
ここまでのクラスたちを“設計目線”で振り返る
いま持っている構造
ざっくり、こんな世界ができています。
- Memo
1つのメモの状態(id, text, done)と、その振る舞い(完了・未完了・バリデーションなど)を担当する。 - MemoList
メモの集まりを管理する(追加・検索・削除・ID 発行など)。 - MemoStorage
MemoList の状態を localStorage に保存・読み込みする。 - MemoAppController
DOM(input, button, list)と MemoList/MemoStorage をつなぐ司令塔。
ここまでは「普通に使えるメモアプリ」になっています。
6日目では、ここに「状態のバリエーション」と「エラーの扱い」を足して、
設計の“耐久性”を上げていきます。
状態が増えると設計は一気に崩れやすくなる
done だけじゃ足りなくなる瞬間
今は Memo に done(完了/未完了)だけがありますが、
現実のアプリを考えると、すぐに欲が出ます。
- 「重要フラグ(priority)」を付けたい
- 「アーカイブ(非表示だけど残しておく)」状態が欲しい
- 「削除済み」も論理的に持っておきたい
こういう状態が増えたときに、
- どこまでを Memo に持たせるか
- どこからを別のクラスや概念に分けるか
を考えられるかどうかが、設計の分かれ目です。
Memo に「優先度」を足すときの考え方
まずはルールを日本語で決める
例えば、優先度(priority)をこう決めます。
- low / normal / high の3種類だけ
- それ以外の文字列は受け付けない
- デフォルトは normal
このときに大事なのは、
「このルールは UI の都合ではなく、“メモという概念のルール”だ」
と捉えることです。
だから、ルールは Memo の中に閉じ込めます。
Memo に priority を追加する
class Memo {
static PRIORITIES = ["low", "normal", "high"];
static validatePriority(priority) {
if (!Memo.PRIORITIES.includes(priority)) {
return { ok: false, message: "priority は low / normal / high のいずれかにしてください" };
}
return { ok: true, message: "" };
}
#id;
#text;
#done;
#priority;
constructor(id, text, priority = "normal") {
const textResult = Memo.validateText(text);
if (!textResult.ok) {
throw new Error("不正なメモの内容です: " + textResult.message);
}
const prioResult = Memo.validatePriority(priority);
if (!prioResult.ok) {
throw new Error("不正な優先度です: " + prioResult.message);
}
this.#id = id;
this.#text = text.trim();
this.#done = false;
this.#priority = priority;
}
getPriority() {
return this.#priority;
}
setPriority(priority) {
const result = Memo.validatePriority(priority);
if (!result.ok) {
console.warn("優先度の更新に失敗:", result.message);
return false;
}
this.#priority = priority;
return true;
}
toString() {
const mark = this.#done ? "[x]" : "[ ]";
const prioMark =
this.#priority === "high" ? "🔥" :
this.#priority === "low" ? "…" :
"";
return `#${this.#id} ${mark} ${prioMark} ${this.#text}`;
}
// ほかのメソッド(getId, getText, setText, isDone, markDone, markUndone)は前と同じ
}
JavaScriptここでのカプセル化のポイントは、
- priority の許容値(low / normal / high)は Memo の中に閉じ込める
- 外からは validatePriority / setPriority / getPriority だけを使う
- UI は「どんな文字列が有効か」を知らなくても、結果だけ見ればいい
という構造になっていることです。
状態が増えたときの責務分離
「優先度で絞り込みたい」という要求が来たら?
今度は MemoList 側に、
「優先度で絞り込む」メソッドを足したくなります。
class MemoList {
// 省略
filterByPriority(priority) {
const result = Memo.validatePriority(priority);
if (!result.ok) {
console.warn("不正な優先度での絞り込み:", result.message);
return [];
}
return this.#memos.filter(memo => memo.getPriority() === priority);
}
}
JavaScriptここでも責務分離が効いています。
- 「priority が有効かどうか」は Memo の責任(validatePriority)
- 「その priority を持つメモだけ集める」のは MemoList の責任
MemoList は Memo の内部構造(#priority)には触らず、
getPriority という窓口だけを使っています。
エラー処理を“ドメイン側で止める”という発想
「ありえない状態」を UI にまで漏らさない
例えば、UI 側でこういうバグがあったとします。
memo.setPriority("urgent"); // 本当は high にしたかった
JavaScriptもし Memo が何もチェックしなければ、
priority に “urgent” が入ってしまい、
アプリのどこかで「high だと思っていたのに違う」というバグが起きます。
しかし、さっきの設計では、
- Memo.validatePriority が false を返す
- setPriority が false を返して更新を拒否する
- コンソールに警告が出る
という形で、「おかしな状態」がドメインの中で止まります。
重要な感覚:
「UI のミスや将来の変更で変な値が来ても、
ドメイン側で“それは受け付けない”と言えるようにしておく」
これが、カプセル化と責務分離が効いている状態です。
Controller は「状態遷移のきっかけ」だけを担当する
UI から見た priority の変更
例えば、画面に「優先度を選ぶセレクトボックス」を置いたとします。
<select id="priority-select">
<option value="low">低</option>
<option value="normal" selected>普通</option>
<option value="high">高</option>
</select>
Controller 側では、こう扱えます。
class MemoAppController {
// 省略
#prioritySelect;
constructor({ input, addButton, listContainer, prioritySelect }) {
this.#memoList = MemoStorage.load();
this.#input = input;
this.#addButton = addButton;
this.#listContainer = listContainer;
this.#prioritySelect = prioritySelect;
this.#setupEvents();
this.#render();
}
#handleAdd() {
const raw = this.#input.value;
const priority = this.#prioritySelect.value;
const textResult = Memo.validateText(raw);
if (!textResult.ok) {
alert(textResult.message);
return;
}
const prioResult = Memo.validatePriority(priority);
if (!prioResult.ok) {
alert(prioResult.message);
return;
}
this.#memoList.add(raw, priority);
this.#input.value = "";
this.#prioritySelect.value = "normal";
MemoStorage.save(this.#memoList);
this.#render();
}
}
JavaScriptここでの責務分離はこうです。
- UI は「どの priority が選ばれたか」を渡すだけ
- その値が有効かどうかは Memo のルールで判定
- Controller は「ルールの結果を見て、進めるか止めるか」を決める
Controller は「状態遷移のきっかけ」を作るだけで、
ルールそのものはドメインに任せています。
永続化にも「状態の増加」が波及する
Storage 側の変更も“責務の範囲内”で収まる
priority を追加したことで、
保存形式も少し変える必要があります。
class MemoStorage {
static save(memoList) {
const data = memoList.getAll().map(memo => ({
id: memo.getId(),
text: memo.getText(),
done: memo.isDone(),
priority: memo.getPriority()
}));
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();
rawArray.forEach(item => {
const priority = item.priority || "normal";
const memo = list.add(item.text, priority);
if (item.done) {
memo.markDone();
}
});
return list;
}
}
JavaScriptここでも責務分離は崩れていません。
- 保存形式(JSON の形)は MemoStorage の責任
- priority のルールは Memo の責任
- priority をどう UI に見せるかは Controller/HTML の責任
状態が増えても、
「どこを直せばいいか」がはっきりしているのが分かると思います。
6日目でいちばん深く理解してほしいこと
今日の本質を、あえて感覚でまとめます。
状態が増えるのは悪いことではない。
問題は、「増えた状態をどこで管理するか」を決めずに
あちこちに if 文をばらまくこと。
class とカプセル化と責務分離をちゃんと使うと、
- ルールはドメイン(Memo)に閉じ込められる
- 集まりとしての操作は MemoList に集約される
- 保存の都合は MemoStorage に隔離される
- UI の都合は Controller/HTML に押し込められる
結果として、
「状態が増えても、直す場所が明確な設計」
になっていきます。
もし余裕があれば、
- priority に「数値(1〜3)」を使うバージョンを考えてみる
- 「アーカイブ」フラグを追加して、一覧からは隠すが保存はする
- 「削除済み」を論理削除にして、Storage からは消さない設計にしてみる
などを、自分なりに
「ルールを決めて → どのクラスの責任かを決めて → カプセル化する」
という流れで試してみてください。
そこまで来ると、もう“サンプルアプリ”ではなく、
「自分で設計したプロダクト」に近づいていきます。


