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;
}
}
JavaScriptAppController 側はこうなります。
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 を少し変えてみる
というリファクタを、実際に手でやってみてください。
その作業の中で、
「クラスの責任ってこういうことか」
「分けておくと、あとから本当に楽だな」
という感覚が、かなりリアルに腹に落ちてくるはずです。


