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

Web APP JavaScript
スポンサーリンク

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 からは消さない設計にしてみる

などを、自分なりに
「ルールを決めて → どのクラスの責任かを決めて → カプセル化する」
という流れで試してみてください。

そこまで来ると、もう“サンプルアプリ”ではなく、
「自分で設計したプロダクト」に近づいていきます。

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