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

Web APP JavaScript
スポンサーリンク

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

6日目のテーマは
「“とりあえず設計できる人”から、“設計を評価・リファクタできる人”になること」です。

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

ここまでで、
それなりにちゃんと動くミニ業務アプリの骨組みはできています。

今日はそこから一歩進んで、

設計力:今の構造の“良いところ/弱いところ”を自分で見抜く
保守性:重くなりそうな部分を先に軽くしておく
実務思考:現場で必ず出てくる「汚れ」をどう扱うか

を、具体的なリファクタ例を通して深掘りします。


「Controller が太り始めた」ときにどう見るか

AppController は太りやすいクラス

ここまでの流れで、AppController はだいたいこんな責任を持っています。

ログイン画面の表示とログイン処理
一覧画面の表示とテーブル描画
詳細画面の表示
編集画面の表示と保存処理
画面の切り替え(表示/非表示)
権限チェックの呼び出し

これ、かなり“太りやすい”です。

最初はいいのですが、
機能が増えるとすぐにこうなります。

メソッドが増えすぎてスクロールが止まらない
似たようなコード(画面切り替え・描画)があちこちに出てくる
どこを直せばいいか分かりにくくなる

6日目では、
この「太り始めた Controller」を
どう分解していくかを考えます。


画面ごとに「小さな View クラス」を切り出す

役割を言葉で分けてみる

AppController の中の責任を、
一度日本語で分解してみます。

ログイン画面の DOM を握っている
ログインボタンのイベントを受けている
ログインフォームの値を読む

一覧画面の DOM を握っている
テーブルを描画している
行クリックで詳細を開いている

詳細画面の DOM を握っている
編集ボタンのイベントを受けている

編集画面の DOM を握っている
入力値をセットしている
保存ボタンのイベントを受けている

ここで見えてくるのは、

「画面ごとに、UI の責任がまとまっている」
ということです。

ならば、それぞれを
小さなクラスに切り出すのが自然です。


LoginView を切り出してみる

UI 専用の小さなクラス

class LoginView {
  #screen;
  #userIdInput;
  #passwordInput;
  #loginButton;

  constructor({ screen, userIdInput, passwordInput, loginButton }) {
    this.#screen = screen;
    this.#userIdInput = userIdInput;
    this.#passwordInput = passwordInput;
    this.#loginButton = loginButton;
  }

  show() {
    this.#screen.style.display = "block";
  }

  hide() {
    this.#screen.style.display = "none";
  }

  onLogin(handler) {
    this.#loginButton.addEventListener("click", () => {
      const userId = this.#userIdInput.value;
      const password = this.#passwordInput.value;
      handler({ userId, password });
    });
  }

  clear() {
    this.#userIdInput.value = "";
    this.#passwordInput.value = "";
  }
}
JavaScript

ここでのポイントは、

LoginView は「ログイン画面の DOM とイベント」だけを担当する
ログインの成否や遷移先は一切知らない
AppController に「ログインボタンが押された」という事実と入力値だけ渡す

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

AppController 側はこうなります。

class AppController {
  #auth;
  #loginView;
  // 他の View も同様に持つ

  constructor(views) {
    this.#auth = new Auth();
    this.#loginView = views.loginView;

    this.#setupEvents();
  }

  #setupEvents() {
    this.#loginView.onLogin(({ userId, password }) => {
      this.#attemptLogin(userId, password);
    });
  }

  #attemptLogin(userId, password) {
    const ok = this.#auth.loginWithIdAndPassword(userId, password);
    if (!ok) {
      alert("ログインに失敗しました");
      this.#loginView.clear();
      return;
    }
    this.#showList();
  }
}
JavaScript

責務分離が一段クリアになりました。

LoginView
→ DOM とイベントの担当

AppController
→ ログイン処理と画面遷移の担当

Auth
→ 認証ロジックの担当


ListView も同じ発想で切り出す

一覧画面の UI をまとめる

class ListView {
  #screen;
  #userInfo;
  #table;
  #onRowClick;

  constructor({ screen, userInfo, table }) {
    this.#screen = screen;
    this.#userInfo = userInfo;
    this.#table = table;
    this.#onRowClick = null;
  }

  show() {
    this.#screen.style.display = "block";
  }

  hide() {
    this.#screen.style.display = "none";
  }

  renderUserInfo(user) {
    this.#userInfo.textContent = `${user.getName()} (${user.getRole()})`;
  }

  renderTable(customers) {
    this.#table.innerHTML = "";
    customers.forEach(customer => {
      const tr = document.createElement("tr");

      const tdId = document.createElement("td");
      tdId.textContent = String(customer.getId());
      tr.appendChild(tdId);

      const tdName = document.createElement("td");
      tdName.textContent = customer.getName();
      tr.appendChild(tdName);

      tr.addEventListener("click", () => {
        if (this.#onRowClick) {
          this.#onRowClick(customer.getId());
        }
      });

      this.#table.appendChild(tr);
    });
  }

  onRowClick(handler) {
    this.#onRowClick = handler;
  }
}
JavaScript

AppController 側はこうなります。

class AppController {
  #auth;
  #customerRepo;
  #loginView;
  #listView;
  // detailView, editView も同様

  constructor({ loginView, listView, detailView, editView }) {
    this.#auth = new Auth();
    this.#customerRepo = new CustomerRepository();
    this.#loginView = loginView;
    this.#listView = listView;
    // 省略
    this.#setupEvents();
  }

  #setupEvents() {
    this.#loginView.onLogin(({ userId, password }) => {
      this.#attemptLogin(userId, password);
    });

    this.#listView.onRowClick(id => {
      this.#showDetail(id);
    });
  }

  #showList() {
    this.#hideAll();
    const user = this.#auth.getCurrentUser();
    if (!Permission.canViewList(user)) {
      this.#showLogin();
      return;
    }
    this.#listView.show();
    this.#listView.renderUserInfo(user);
    const customers = this.#customerRepo.findAll();
    this.#listView.renderTable(customers);
  }
}
JavaScript

ここでの設計の良さは、

一覧の DOM 構造が変わっても ListView だけ直せばいい
AppController は「どの顧客を表示するか」だけ考えればいい
CustomerRepository は DOM を一切知らない

という三層分離が、よりはっきりしたことです。


「View を切り出す」ことの実務的メリット

画面デザイン変更が怖くなくなる

現場で必ず起きるのが、

「デザイナーさんが HTML/CSS を大きく変えてくる」
「テーブルをカード表示に変えたい」
「ログイン画面のレイアウトをガラッと変えたい」

という変更です。

View をクラスとして切り出しておくと、

ログイン画面の変更 → LoginView だけを見ればいい
一覧画面の変更 → ListView だけを見ればいい

という状態になります。

AppController やドメイン層(Auth, User, Customer, Repository)は
一切触らなくて済む。

これは、
「UI とロジックを分ける」という教科書の言葉が、
現場でどう効くかの具体例
です。


「Controller がやるべきこと」をもう一度言語化する

Controller の責任は“判断と調整”だけにする

View を切り出したことで、
AppController の責任はよりシンプルにできます。

ログインするかどうかを判断する
どの画面を表示するかを決める
どのデータを View に渡すかを決める
どの権限チェックをいつ行うかを決める

逆に言うと、

DOM を直接触らない
HTML の構造を知らない
CSS のクラス名を知らない

ここまで徹底できると、
Controller は「アプリのシナリオ」だけを書く場所になります。

実務思考で言えば、

仕様書に書いてある「画面遷移図」「ユースケース」を
そのままコードに落とし込む場所が Controller

というイメージです。


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

今日の本質は、
「太り始めたクラスを、役割ごとに“分解し直す目”を持つこと」です。

AppController が太ってきたら、

これは UI の責任だな → View に切り出そう
これはデータの責任だな → Repository/ドメインに寄せよう
これは権限の責任だな → Permission に寄せよう

と、責務の場所を移し替えていく。

それがそのまま、
設計力の成長であり、
保守性の向上であり、
実務思考そのものです。

もし余裕があれば、

DetailView と EditView も自分でクラスに切り出してみる
AppController から「DOM に触っている行」を全部なくしてみる
View クラスだけを差し替えて、UI を少し変えてみる

というリファクタを、実際に手でやってみてください。

その作業の中で、
「クラスの責任ってこういうことか」
「分けておくと、あとから本当に楽だな」
という感覚が、かなりリアルに腹に落ちてくるはずです。

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