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

Web APP JavaScript
スポンサーリンク

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

5日目のテーマは
「“なんとなく動く設計”から、“変更に強い設計”へ一段引き上げること」です。

ログイン画面
一覧 → 詳細 → 編集
権限制御(擬似)

ここまでで、ひと通りの流れは作れている前提で、今日は

設計力:クラスの責任の境界をもう一段クリアにする
保守性:変更が来たときに“直す場所”がすぐ分かる構造にする
実務思考:現場で本当に起きる「仕様変更」を想定して設計を見直す

ここをじっくりやります。


「仕様変更が来た」と仮定して設計をチェックする

想定する変更 1:ログイン方法が変わる

例えば、こういう変更が来たとします。

ユーザーID+パスワードではなく、
メールアドレス+パスワードでログインしたい。
将来的には「外部サービスログイン(Google など)」も検討したい。

このとき、今の設計で「どこを直すか」がすぐ言えれば、
設計はかなり良い状態です。

Auth クラスの login の中身
User の持つ情報(id, name, role に加えて email)
ログイン画面の入力欄(userId → email)
AppController の attemptLogin の引数

逆に言うと、
ここ以外は触りたくないし、触らなくて済むべきです。

もし画面のあちこちで

auth.login(userId, password);
user.getId() を前提にした処理

が散らばっているなら、
それは「設計力不足のサイン」です。


Auth を「認証の窓口」として育てる

認証方法を増やせる形にしておく

今はこうだとします。

class Auth {
  #currentUser;

  constructor() {
    this.#currentUser = null;
  }

  login(userId, password) {
    if (userId === "admin" && password === "pass") {
      this.#currentUser = new User(1, "管理者", "admin");
      return true;
    }
    if (userId === "staff" && password === "pass") {
      this.#currentUser = new User(2, "一般社員", "staff");
      return true;
    }
    return false;
  }

  logout() {
    this.#currentUser = null;
  }

  isLoggedIn() {
    return this.#currentUser !== null;
  }

  getCurrentUser() {
    return this.#currentUser;
  }
}
JavaScript

これを、将来の拡張を見据えて少し整理します。

ポイントは、

「認証の方法」と
「ログイン状態の保持」を
分けて考えることです。

class Auth {
  #currentUser;

  constructor() {
    this.#currentUser = null;
  }

  loginWithIdAndPassword(userId, password) {
    const user = AuthUserStore.findByIdAndPassword(userId, password);
    if (!user) return false;
    this.#currentUser = user;
    return true;
  }

  loginWithEmailAndPassword(email, password) {
    const user = AuthUserStore.findByEmailAndPassword(email, password);
    if (!user) return false;
    this.#currentUser = user;
    return true;
  }

  logout() {
    this.#currentUser = null;
  }

  isLoggedIn() {
    return this.#currentUser !== null;
  }

  getCurrentUser() {
    return this.#currentUser;
  }
}
JavaScript

ここで AuthUserStore は、
「認証用のユーザー情報を探す役割」を持つクラス(あるいはモジュール)です。

重要なのは、

Auth は「どう探すか」を知らない
AuthUserStore は「ログイン状態」を知らない

という分離ができていることです。

これにより、

認証方法を増やしたい → AuthUserStore を拡張
ログイン状態の扱いを変えたい → Auth を修正

と、直す場所がはっきり分かれます。


「一覧の仕様変更」に耐えられる設計かをチェックする

想定する変更 2:一覧にフィルタとソートを追加したい

例えば、こういう要求が来たとします。

名前で検索したい
メールアドレスで絞り込みたい
名前順・作成日順でソートしたい

このとき、
CustomerRepository と AppController の責任分担が
きちんとできているかが問われます。

CustomerRepository は
「データの集まりに対する操作」を担当します。

class CustomerRepository {
  #customers;

  constructor() {
    this.#customers = [
      // 省略
    ];
  }

  findAll() {
    return this.#customers;
  }

  findById(id) {
    return this.#customers.find(c => c.getId() === id) || null;
  }

  searchByName(keyword) {
    const k = keyword.trim();
    if (k === "") return this.#customers;
    return this.#customers.filter(c => c.getName().includes(k));
  }

  // ソートはどうするか?
}
JavaScript

ここで設計の分かれ道があります。

ソートのロジックを Repository に入れるか、
Controller に入れるか。

実務思考で考えると、

「ソートの基準が“データの意味”に関わるなら Repository」
「ソートの基準が“画面の都合”なら Controller」

という分け方がしっくりきます。

例えば、

作成日が新しい順 → データの意味に関わる
画面の列をクリックした順 → UI の都合

なので、

作成日順の取得は Repository に
「今どの列でソートしているか」は Controller に

という分担が自然です。


一覧のフィルタとソートを Controller から使う

Controller 側の責務を整理する

AppController は、
「ユーザーの操作に応じて、どの Repository メソッドを使うか」を決めます。

例えば、名前検索とソートを組み合わせるとします。

class AppController {
  // 省略
  #searchInput;
  #sortSelect;

  constructor(dom) {
    // 省略
    this.#searchInput = dom.searchInput;
    this.#sortSelect = dom.sortSelect;
    this.#setupEvents();
  }

  #setupEvents() {
    this.#searchInput.addEventListener("input", () => {
      this.#renderCustomerTable();
    });

    this.#sortSelect.addEventListener("change", () => {
      this.#renderCustomerTable();
    });
  }

  #renderCustomerTable() {
    this.#customerTable.innerHTML = "";

    const keyword = this.#searchInput.value;
    const sortKey = this.#sortSelect.value;

    let customers = this.#customerRepo.searchByName(keyword);

    if (sortKey === "name") {
      customers = [...customers].sort((a, b) =>
        a.getName().localeCompare(b.getName())
      );
    }

    customers.forEach(customer => {
      // 行を作って追加(前回と同じ)
    });
  }
}
JavaScript

ここでの責務分離はこうです。

検索(どの顧客が対象か) → Repository
ソート(どの順番で表示するか) → Controller(UI の都合)

この線引きができていると、
「検索条件が増えた」「ソート条件が増えた」ときに
直す場所が迷子になりません。


「権限の仕様変更」に耐えられる設計かをチェックする

想定する変更 3:権限ルールが細かくなる

例えば、こういう変更が来たとします。

admin:すべての顧客を編集可能
staff:自分が担当の顧客だけ編集可能
guest:一覧は見られるが詳細は見られない

このとき、Permission と User の設計が試されます。

User に「担当顧客IDのリスト」を持たせるか?
Customer に「担当者ID」を持たせるか?
Permission は「誰と何の組み合わせ」を見て判断するか?

例えば、こういう形が考えられます。

class User {
  #id;
  #name;
  #role;
  #assignedCustomerIds;

  constructor(id, name, role, assignedCustomerIds = []) {
    this.#id = id;
    this.#name = name;
    this.#role = role;
    this.#assignedCustomerIds = assignedCustomerIds;
  }

  isAdmin() {
    return this.#role === "admin";
  }

  isStaff() {
    return this.#role === "staff";
  }

  isAssignedTo(customer) {
    return this.#assignedCustomerIds.includes(customer.getId());
  }
}
JavaScript

Permission はこうなります。

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

  static canViewDetail(user, customer) {
    if (!user) return false;
    if (user.isAdmin()) return true;
    if (user.isStaff()) return user.isAssignedTo(customer);
    return false;
  }

  static canEditCustomer(user, customer) {
    if (!user) return false;
    if (user.isAdmin()) return true;
    if (user.isStaff()) return user.isAssignedTo(customer);
    return false;
  }
}
JavaScript

Controller 側は、
「誰が」「どの顧客に対して」操作しようとしているかを渡すだけです。

const user = this.#auth.getCurrentUser();
const customer = this.#customerRepo.findById(id);

if (!Permission.canViewDetail(user, customer)) {
  alert("この顧客の詳細を見る権限がありません");
  this.#showList();
  return;
}
JavaScript

ここでの設計のキモは、

権限ロジックは Permission に集約
User は「自分に関する情報」を知っている
Customer は「自分に関する情報」を知っている
Controller は「誰が」「何に対して」操作するかをつなぐだけ

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


実務思考:テストしやすい設計になっているか?

「ブラウザを開かなくても確認できる部分」を増やす

実務で効く設計は、
「ブラウザを開かなくてもロジックをテストしやすい」
という特徴を持っています。

例えば、Permission のテストは
コンソールだけでできます。

const admin = new User(1, "管理者", "admin");
const staff = new User(2, "社員", "staff", [1, 3]);
const guest = null;

const c1 = new Customer(1, "A社", "a@example.com");
const c2 = new Customer(2, "B社", "b@example.com");

console.log(Permission.canEditCustomer(admin, c1)); // true
console.log(Permission.canEditCustomer(staff, c1)); // true
console.log(Permission.canEditCustomer(staff, c2)); // false
console.log(Permission.canEditCustomer(guest, c1)); // false
JavaScript

Auth のテストも同様です。

Customer のバリデーションも、
ブラウザ抜きで試せます。

これは全部、
ドメイン層と UI 層をきれいに分けているからこそできることです。

テストしやすい設計は、
そのまま「保守しやすい設計」です。


5日目でいちばん深く持ってほしい感覚

今日の本質は、
「仕様変更を仮定して設計を見直す」ことです。

ログイン方法が変わったら、どこを直す?
一覧に検索・ソートが増えたら、どこを直す?
権限ルールが細かくなったら、どこを直す?

この問いに対して、

ここ、とここだけ
他は触らなくていい

と自信を持って言える構造になっていれば、
設計力・保守性・実務思考はちゃんと噛み合っています。

もし余裕があれば、

ログイン方法を「メール+パスワード」に変える
一覧に「名前検索」を実装する
staff の権限ルールを自分なりに変えてみる

などを、
「どのクラスの責任か?」を意識しながら
実際に手を動かしてみてください。

その“変更してみる体験”が、
設計を本当に自分のものにしてくれます。

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