基礎から学ぶJavaScript入門 90日コース | 実践的なJavaScript - Day 72:コード設計

JavaScript 90日で身につけるJavaScript
スポンサーリンク
スポンサーリンク

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;
}
JavaScript

throw 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処理は画面への表示を担当します。そして、全体の流れを管理する処理がそれらを順番に呼び出します。

大切なのは、分割すること自体を目的にしないことです。「何を変更したいとき、どこを見ればよいか」がわかり、コードを読みやすく保てるように分けます。今回の例を動かし、検索条件や表示項目を少しずつ変更してみると、役割ごとに分ける効果を実感しやすくなります。

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