Day 72:コード設計
Day 72では、コードを役割ごとに分けて設計する方法を学びます。今回のテーマは、関数を「UI処理」「データ処理」「API処理」に分けることです。Day 71ではES Modulesを使ってファイルを分割しました。Day 72では、そのファイルや関数をどのような役割で分けると読みやすく、修正しやすくなるのかを考えます。
コード設計は、難しいルールを暗記することではありません。大切なのは、処理を読んだときに「この関数は何をするものか」がわかりやすいことです。ひとつの関数がAPI通信、検索、画面表示、エラー処理をすべて担当していると、コードが長くなるにつれて修正箇所を見つけにくくなります。そこで、役割ごとに処理を分け、必要な部分を組み合わせてアプリを作ります。
今回は、ユーザー一覧をAPIから取得し、入力したキーワードで検索して画面に表示する小さなアプリを例にします。API処理、データ処理、UI処理を分け、最後に全体の流れを担当する関数でつなぎます。
コード設計で目指すこと
今回の目標は、コードを次の3つの役割に分けることです。
- API処理:外部からデータを取得します。
- データ処理:取得したデータを検索・加工します。
- UI処理:画面にメッセージや検索結果を表示します。
それぞれの関数は、できるだけ一つの役割に集中させます。たとえば、APIからデータを取得する関数が、画面の文字色まで変更する必要はありません。また、画面表示の関数がAPIへ通信する必要もありません。
このように役割を分けると、変更の影響範囲を小さくできます。表示方法を変えるときはUI処理を、検索ルールを変えるときはデータ処理を確認すればよくなります。
まず処理の流れを整理する
コードを書く前に、アプリの流れを日本語で整理します。今回は次のようになります。
- アプリを起動したら、APIからユーザー一覧を取得します。
- 取得した一覧をアプリ内に保存します。
- 検索ボタンが押されたら、入力キーワードを読み取ります。
- 保存してある一覧から、キーワードに合うユーザーを探します。
- 検索結果を画面へ表示します。
- 通信に失敗した場合は、ユーザー向けのメッセージを表示します。
この流れを先に考えると、「どの処理をどの関数に置くか」を決めやすくなります。
ひとつの関数に詰め込みすぎるとどうなるか
初心者のうちは、すべての処理を一つの関数へ書くことがあります。小さなプログラムなら動くこともありますが、コードが増えると読みにくくなります。
// 学習用の悪い例:通信、検索、表示を1つの関数に詰め込んでいる
async function doEverything() {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
const keyword = document.getElementById("keyword").value.trim();
const results = users.filter(function (user) {
return user.name.includes(keyword);
});
const list = document.getElementById("results");
list.innerHTML = "";
results.forEach(function (user) {
const li = document.createElement("li");
li.textContent = user.name;
list.appendChild(li);
});
}JavaScriptこの関数には、API通信、入力値取得、検索、画面表示が混在しています。たとえば検索方法だけを変更したい場合でも、通信や表示のコードが並ぶ中から該当箇所を探さなければなりません。
役割ごとに関数を分ける
今回は次のように分けます。
- `fetchUsers()`:APIからユーザー一覧を取得します。
- `filterUsers()`:ユーザー一覧をキーワードで絞り込みます。
- `renderUsers()`:検索結果を画面に表示します。
- `setMessage()`:画面の状態メッセージを更新します。
- `startApp()`:全体の流れを組み立てます。
関数名を見ただけで役割がわかるようにするのも、コード設計の大切な工夫です。
API処理:データを取得する
API処理の役目は、外部サービスへ通信してデータを受け取ることです。この関数では、HTML要素を操作しません。取得したデータを呼び出し元へ返すところまでを担当します。
api.js を作成し、次のコードを書きます。
// APIからユーザー一覧を取得する
export async function fetchUsers() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
// fetchはHTTPエラー時に自動で例外にならないため確認する
if (!response.ok) {
throw new Error(`通信に失敗しました(${response.status})`);
}
// JSONを読み取り、ユーザー一覧として返す
const users = await response.json();
// 予想外のデータ形式を早めに検出する
if (!Array.isArray(users)) {
throw new Error("ユーザー一覧の形式が正しくありません。");
}
return users;
}JavaScriptthrow new Error() は、問題が起きたことを呼び出し元へ伝える書き方です。ここではAPI処理の関数がエラーを検出し、アプリ全体の流れを担当する関数が後で受け止めます。
データ処理:検索結果を作る
次はデータ処理です。ここでは、受け取った配列から条件に合う要素を選びます。API通信やDOM操作は行いません。
data.js を作成します。
// キーワードに合うユーザーだけを返す
export function filterUsers(users, keyword) {
// 前後の空白を除き、大文字・小文字を区別しないようにする
const normalizedKeyword = keyword.trim().toLowerCase();
// キーワードが空なら一覧をそのまま返す
if (normalizedKeyword === "") {
return users;
}
// 名前またはメールアドレスにキーワードを含むユーザーを選ぶ
return users.filter(function (user) {
const name = (user.name || "").toLowerCase();
const email = (user.email || "").toLowerCase();
return (
name.includes(normalizedKeyword) ||
email.includes(normalizedKeyword)
);
});
}JavaScriptこの関数は、受け取ったデータを条件に合わせて加工し、結果を返します。画面へ表示する処理がないため、ブラウザーのDOMに依存せず、検索ルールだけを確認できます。
初心者の方は、「データ処理はデータを受け取り、別のデータを返す」と考えると整理しやすいです。
UI処理:画面を更新する
UI処理の役目は、画面への表示です。APIを呼び出したり、検索条件を決めたりせず、受け取った内容をDOMへ反映します。
ui.js を作成します。
// 状態メッセージを画面に表示する
export function setMessage(element, text, type = "") {
element.textContent = text;
// メッセージ用の要素に状態クラスを設定する
element.className = type;
}
// ユーザー一覧を画面に表示する
export function renderUsers(container, users) {
// 前回の結果を安全に取り除く
container.replaceChildren();
// 該当データがない場合は呼び出し元へ知らせる
if (users.length === 0) {
return;
}
users.forEach(function (user) {
// 表示用の li 要素を作る
const item = document.createElement("li");
// 外部から取得した文字列はtextContentで表示する
item.textContent = `${user.name} / ${user.email}`;
// 作成した要素を一覧へ追加する
container.appendChild(item);
});
}JavaScriptここでは innerHTML へAPIデータを直接埋め込まず、textContent で表示しています。外部から取得した文字列をHTMLとして解釈させないための基本的な安全策です。
全体の流れ:各処理を組み合わせる
最後に、API処理、データ処理、UI処理をつなぎます。この役割を担当するのが app.js です。
app.js では、状態としてユーザー一覧を保持し、検索ボタンが押されたときに絞り込みと表示更新を行います。
// 各ファイルから必要な関数を読み込む
import { fetchUsers } from "./api.js";
import { filterUsers } from "./data.js";
import { renderUsers, setMessage } from "./ui.js";
// HTML要素を取得する
const keywordInput = document.getElementById("keywordInput");
const searchButton = document.getElementById("searchButton");
const message = document.getElementById("message");
const resultList = document.getElementById("resultList");
const loading = document.getElementById("loading");
// APIから取得した元データを保持する
let allUsers = [];
// キーワードに応じて結果を更新する
function updateResults() {
const keyword = keywordInput.value;
const matchedUsers = filterUsers(allUsers, keyword);
// 検索結果を画面に描画する
renderUsers(resultList, matchedUsers);
// 件数または0件のメッセージを表示する
if (matchedUsers.length === 0) {
setMessage(message, "条件に合うユーザーが見つかりませんでした。");
return;
}
setMessage(message, `${matchedUsers.length}件見つかりました。`);
}
// 検索ボタンの処理を登録する
searchButton.addEventListener("click", function () {
updateResults();
});
// APIからデータを取得してアプリを開始する
async function startApp() {
try {
// 通信中の状態を画面に表示する
loading.hidden = false;
setMessage(message, "ユーザー情報を読み込み中です...");
// API処理にデータ取得を任せる
allUsers = await fetchUsers();
// 初期状態の一覧を表示する
updateResults();
} catch (error) {
// 画面には利用者向けの簡潔な案内を表示する
setMessage(
message,
"データを読み込めませんでした。時間をおいて再度お試しください。",
"error"
);
// 詳細は開発者が確認できるようコンソールへ出す
console.error("ユーザー情報の取得エラー:", error);
} finally {
// 成功・失敗にかかわらず読み込み表示を消す
loading.hidden = true;
}
}
// ページ読み込み後にアプリを開始する
startApp();JavaScriptこの app.js は、それぞれの処理を自分で細かく実行するというより、どの順番で関数を呼ぶかを管理しています。これが「全体の流れを組み立てる役割」です。
HTMLとファイル構成
ファイルを次のように配置します。
project/
├── index.html
└── js/
├── app.js
├── api.js
├── data.js
└── ui.js
index.html は次のようにします。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Day 72 コード設計</title>
<style>
/* エラー状態を見分けやすくする */
.error {
color: #b00020;
}
</style>
</head>
<body>
<h1>ユーザー検索</h1>
<!-- 検索に使う入力欄とボタン -->
<input id="keywordInput" type="search" placeholder="名前やメールで検索">
<button id="searchButton" type="button">検索</button>
<!-- 通信中にだけ表示する案内 -->
<p id="loading" hidden>読み込み中です...</p>
<!-- 結果件数やエラーを案内する場所 -->
<p id="message">ユーザー情報を読み込みます。</p>
<!-- 検索結果を表示する場所 -->
<ul id="resultList"></ul>
<!-- app.jsをESモジュールとして読み込む -->
<script type="module" src="./js/app.js"></script>
</body>
</html>
この構成では、app.js から同じ js フォルダー内の各ファイルを読み込んでいます。ブラウザーでES Modulesを使う場合、開発用のローカルサーバーでページを開くと、ファイル読み込みの問題を避けやすくなります。また、import のパスは大文字・小文字も含めて実際のファイル名と一致させ、.js まで書きます。
役割分担をもう一度整理する
今回のファイル構成では、各ファイルの役割を次のように分けています。
- `api.js` は、APIへ通信し、データを返します。
- `data.js` は、データを検索条件に合わせて加工します。
- `ui.js` は、メッセージや検索結果を画面に表示します。
- `app.js` は、必要な関数を呼び出して処理全体を進めます。
- `index.html` は、画面の基本的な部品を用意します。
それぞれの役割が分かれているため、たとえば検索対象を「名前だけ」から「名前とメールアドレス」に変える場合は、主に data.js を確認すればよくなります。表示形式を変える場合は ui.js、APIの接続先を変える場合は api.js が中心になります。
重要な注意点:関数を細かく分けすぎない
役割ごとに分けることは大切ですが、すべての1行を関数にする必要はありません。関数を細かく作りすぎると、処理を追うために何度も別の場所へ移動することになり、かえって読みにくくなる場合があります。
- 関数名から処理の目的がわかる
- 関数がひとつのまとまった仕事を担当している
- 変更するときに影響する範囲が見つけやすい
このあたりを目安に、適度な単位で分けることが大切です。コード設計に唯一の正解があるわけではありません。アプリの規模やチームのルールに合わせて、読みやすさを保てる分け方を選びます。
エラー処理の役割分担
エラーがどこで起きたかを確認するための情報と、画面に出す案内は分けて考えます。API処理では、通信に失敗したことを throw で呼び出し元に伝え、app.js がそれを catch で受け止めています。そして、画面には利用者に伝わる簡潔な案内を表示し、詳細は console.error() で確認できるようにしています。
内部のエラー内容をそのまま利用者向け画面に表示すると、難しい情報が出たり、内部の実装情報を不用意に見せたりすることがあります。初心者のうちは、利用者向けメッセージと開発者向け詳細を分ける習慣をつけるとよいです。
Day 72の練習課題
基本のテンプレートが動いたら、次のような小さな変更に挑戦してみてください。
- `filterUsers()` を変更し、会社名でも検索できるようにする
- `renderUsers()` に電話番号の表示を追加する
- 検索結果が0件のとき、結果リストの代わりに案内文を表示する
- `api.js` のURLをわざと間違え、エラー処理の動きを確認する
- 検索ボタンだけでなく、Enterキーでも検索できるようにする
変更するときは、一度に複数の場所を書き換えるのではなく、「どの役割を変えるのか」を考えてから取り組みます。検索条件ならデータ処理、見た目ならUI処理、通信先ならAPI処理を中心に変更するのが基本です。
Day 72のゴール
Day 72では、次のことができるようになっていれば十分です。
- 処理をAPI・データ・UIの役割に分けて考えられる
- 関数名から役割が伝わるようにできる
- 取得・加工・表示の処理を別々に実装できる
- 全体の流れを担当する関数で各処理をつなげられる
- エラー情報と利用者向けメッセージを区別できる
まとめ
Day 72では、コード設計の基本として、関数やファイルをUI処理、データ処理、API処理に分ける方法を学びました。API処理はデータの取得、データ処理は検索や加工、UI処理は画面への表示を担当します。そして、全体の流れを管理する処理がそれらを順番に呼び出します。
大切なのは、分割すること自体を目的にしないことです。「何を変更したいとき、どこを見ればよいか」がわかり、コードを読みやすく保てるように分けます。今回の例を動かし、検索条件や表示項目を少しずつ変更してみると、役割ごとに分ける効果を実感しやすくなります。
