JavaScript | 1 日 120 分 × 7 日アプリ学習:ミニ業務アプリ(総合)

Web APP JavaScript
スポンサーリンク

4日目のゴールと今日のテーマ

4日目のテーマは
「“とりあえず動く業務アプリ”から、“壊れにくい業務アプリ”に育てる設計」です。

同じログイン/一覧 → 詳細 → 編集/権限制御でも、
今日は特に

  • 入力チェック(バリデーション)をどこに書くか
  • エラーメッセージをどこで決めるか
  • 「想定外の状態」をどう扱うか

を、設計力・保守性・実務思考の観点から深掘りします。

コードを書くというより、
「この if はどのクラスに属するべきか?」を徹底的に考える日です。


バリデーションは「画面の都合」ではなく「ドメインのルール」

ありがちな“全部画面に書く”パターン

例えば、編集画面で名前とメールを入力させるとします。

よくあるのは、編集画面側でこう書くパターンです。

if (name === "") {
  alert("名前は必須です");
  return;
}
if (!email.includes("@")) {
  alert("メールアドレスの形式が不正です");
  return;
}
JavaScript

一見普通ですが、これをやると

  • 別の画面から顧客を作るときに、同じチェックをコピペする
  • 仕様変更(「名前は2文字以上」など)が入ると、全部探して直す
  • どこか1か所だけ直し忘れてバグになる

という“業務アプリあるある地獄”に入ります。

ここで大事なのは、

「名前やメールのルールは“顧客という概念”の一部」
という視点です。

だから、本来は Customer クラス側に閉じ込めるべきです。


Customer に「ルール」を閉じ込める設計

バリデーション専用メソッドを持たせる

Customer クラスを、少し“賢く”します。

class Customer {
  #id;
  #name;
  #email;

  constructor(id, name, email) {
    const result = Customer.validate(name, email);
    if (!result.ok) {
      throw new Error("不正な顧客データ: " + result.message);
    }
    this.#id = id;
    this.#name = name;
    this.#email = email;
  }

  static validate(name, email) {
    if (!name || name.trim() === "") {
      return { ok: false, message: "名前は必須です" };
    }
    if (name.trim().length < 2) {
      return { ok: false, message: "名前は2文字以上で入力してください" };
    }
    if (!email || email.trim() === "") {
      return { ok: false, message: "メールアドレスは必須です" };
    }
    if (!email.includes("@")) {
      return { ok: false, message: "メールアドレスの形式が不正です" };
    }
    return { ok: true, message: "" };
  }

  update(name, email) {
    const result = Customer.validate(name, email);
    if (!result.ok) {
      return result;
    }
    this.#name = name;
    this.#email = email;
    return { ok: true, message: "" };
  }

  getId() {
    return this.#id;
  }

  getName() {
    return this.#name;
  }

  getEmail() {
    return this.#email;
  }
}
JavaScript

ここでの重要ポイントは、はっきりこうです。

  • 「何が有効な名前・メールか」というルールは Customer の責任
  • 画面側はルールの中身を知らなくてよく、結果(ok / message)だけ見ればいい
  • ルールが変わっても Customer だけ直せばよい

これが、設計力と保守性が効いている状態です。


エラーメッセージは「どこで決めるか」を分けて考える

ルールのメッセージと、UI のメッセージは別物

さっきの Customer.validate は、
すでに message を返しています。

では、画面側ではどう使うのが自然でしょうか。

#saveCustomer(id, newName, newEmail) {
  const user = this.#auth.getCurrentUser();
  if (!Permission.canEdit(user)) {
    alert("編集権限がありません");
    this.#showDetail(id);
    return;
  }

  const customer = this.#customerRepo.findById(id);
  if (!customer) {
    alert("対象の顧客が見つかりません");
    this.#showList();
    return;
  }

  const result = customer.update(newName, newEmail);
  if (!result.ok) {
    alert(result.message);
    return;
  }

  this.#customerRepo.save(customer);
  this.#showDetail(id);
}
JavaScript

ここでの分担はこうなります。

  • 「何がダメか」を決めるのは Customer(ドメイン)
  • 「どうユーザーに伝えるか」(alert にするか、画面に表示するか)は Controller(UI)

つまり、

ルールのメッセージ(ドメインの言葉)

表示の仕方(UI の都合)

を分けている、ということです。

実務では、
「エラーメッセージを画面の下に出したい」
「英語版も欲しい」
などの要求が出てきます。

そのとき、
ルールと UI が混ざっていると地獄を見ます。
分かれていれば、UI 側だけ差し替えれば済みます。


「想定外の状態」をどう扱うかも設計の一部

ありえないはずのケースは、本当にありえないか?

例えば、詳細画面でこう書いていたとします。

const customer = this.#customerRepo.findById(id);
console.log(customer.getName());
JavaScript

「一覧から来ているんだから、必ず存在するでしょ」
と油断すると、実務では普通に事故ります。

  • 別タブで開いている間にデータが削除された
  • データ移行で ID が変わった
  • URL を直接叩かれた

など、「ありえないと思っていた状態」は
現場では普通に起きます。

だからこそ、Controller では必ずこう書きます。

const customer = this.#customerRepo.findById(id);
if (!customer) {
  alert("対象のデータが見つかりません");
  this.#showList();
  return;
}
JavaScript

ここでの設計ポイントは、

  • 「存在しない ID を渡されたら null を返す」のは Repository の責任
  • 「そのときどう振る舞うか」を決めるのは Controller の責任

という分担を守っていることです。


権限制御を「仕様変更に強い形」にする

Permission クラスを“if の墓場”にしない

Permission をこう書いていたとします。

class Permission {
  static canViewList(user) {
    return !!user;
  }

  static canViewDetail(user) {
    return !!user;
  }

  static canEdit(user) {
    if (!user) return false;
    return user.isAdmin();
  }
}
JavaScript

ここまではシンプルですが、
実務ではすぐにこうなります。

  • 「staff も一部編集できるようにしてほしい」
  • 「特定の顧客だけ編集禁止にしたい」
  • 「自分が担当の顧客だけ編集可にしたい」

このとき、
Permission の中身をどう育てるかが“設計力”です。

例えば、こういう方向性があります。

class Permission {
  static canEditCustomer(user, customer) {
    if (!user) return false;
    if (user.isAdmin()) return true;
    if (user.isStaff()) {
      // 例えば「担当者IDが一致する顧客だけ編集可」など
      return customer.getId() % 2 === 0; // 擬似ルール
    }
    return false;
  }
}
JavaScript

Controller 側はこう変わります。

const customer = this.#customerRepo.findById(id);
if (!customer) {
  alert("対象の顧客が見つかりません");
  this.#showList();
  return;
}

if (!Permission.canEditCustomer(user, customer)) {
  alert("この顧客を編集する権限がありません");
  this.#showDetail(id);
  return;
}
JavaScript

ここでの大事なポイントは、

  • 権限ロジックは Permission に集約されている
  • Controller は「誰が」「どの顧客に対して」操作するかを渡すだけ
  • 仕様変更が来ても Permission を中心に直せばよい

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


「実務思考」とは“未来の自分を楽させる思考”

今だけ動けばいいコードと、育てられるコード

4日目まで来ると、
すでに「動くミニ業務アプリ」は作れています。

でも、実務思考で一番大事なのは、

「このアプリに、1年後・2年後に機能追加が来たとき、
自分は泣かずに済むか?」

という視点です。

今日やったことは、全部そこにつながっています。

  • バリデーションを Customer に閉じ込める
    → 入力ルールが変わっても、1か所直せばいい
  • エラーメッセージの“ルール”と“表示方法”を分ける
    → UI を変えても、ドメインのルールはそのまま
  • 「存在しないデータ」「権限不足」を Controller で必ず扱う
    → 想定外の状態でもアプリが落ちない
  • 権限ロジックを Permission に集約する
    → 仕様変更が来ても if 文を全画面から探さなくていい

これが、
設計力・保守性・実務思考が
一つにつながった状態です。


4日目の締め:if 文の“居場所”を意識する

最後に、今日の一番大事な問いを一つだけ残します。

「この if は、本当はどのクラスの事情を見ている?」

名前やメールのルールを見ているなら → Customer
顧客の集まりを見ているなら → Repository
ユーザーの権限を見ているなら → User / Permission
画面の見せ方を見ているなら → Controller / UI

この問いを癖にしておくと、
if 文が迷子にならなくなります。

それがそのまま、
“壊れにくい業務アプリ”を作る力になります。

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