JavaScript | ゼロからはじめるプログラミング、30日で基礎を学ぶJavaScript:ミニWebアプリ開発 - Day29:TODOアプリ⑤

JavaScript JavaScript
スポンサーリンク

Day29 後半のゴール

前半で、TODOアプリのコードを
「データ操作」「描画」「保存・復元」「イベント処理」
という役割ごとに分けるところまで整理しました。

後半では、そこからもう一歩踏み込んで、

重複しているコードを減らす
関数の大きさと役割のバランスを整える
「読んだときに流れがスッと頭に入る」コードに並べ直す

という、実務寄りのコード整理をやっていきます。


まず「どこが読みにくいか」を言葉にしてみる

なんとなくモヤっとする場所を具体化する

コード整理で一番大事なのは、
「なんとなく嫌だ」を「ここがこう嫌だ」に変えることです。

例えば、今の TODO アプリのコードを眺めると、こんなモヤっとが出てきます。

renderTasks の中にイベント登録が全部入っていて長い
チェックボックスと削除ボタンの処理が毎回同じように書かれている
タスク1件分の DOM を作る処理が、forEach の中にベタッと書かれている

これを放置すると、
機能を足すたびに renderTasks が肥大化していきます。

Day29 後半では、ここを「小さな部品」に分けていきます。


タスク1件分の要素を作る関数を切り出す

createTaskItemElement という「部品」を作る

renderTasks の中には、タスク1件分の DOM を作る処理がまとまっています。

const taskItemElement = document.createElement("div");

const checkboxElement = document.createElement("input");
checkboxElement.type = "checkbox";
checkboxElement.checked = task.isDone;

const taskTextElement = document.createElement("span");
taskTextElement.textContent = task.title;

if (task.isDone) {
  taskTextElement.style.textDecoration = "line-through";
  taskTextElement.style.color = "#888";
} else {
  taskTextElement.style.textDecoration = "none";
  taskTextElement.style.color = "inherit";
}

const deleteButtonElement = document.createElement("button");
deleteButtonElement.textContent = "削除";

checkboxElement.addEventListener("change", () => {
  toggleTaskDone(index);
  saveTasks();
  renderTasks();
});

deleteButtonElement.addEventListener("click", () => {
  deleteTask(index);
  saveTasks();
  renderTasks();
});

taskItemElement.appendChild(checkboxElement);
taskItemElement.appendChild(taskTextElement);
taskItemElement.appendChild(deleteButtonElement);
JavaScript

これを丸ごと関数にしてしまいます。

function createTaskItemElement(task, index) {
  const taskItemElement = document.createElement("div");

  const checkboxElement = document.createElement("input");
  checkboxElement.type = "checkbox";
  checkboxElement.checked = task.isDone;

  const taskTextElement = document.createElement("span");
  taskTextElement.textContent = task.title;

  if (task.isDone) {
    taskTextElement.style.textDecoration = "line-through";
    taskTextElement.style.color = "#888";
  } else {
    taskTextElement.style.textDecoration = "none";
    taskTextElement.style.color = "inherit";
  }

  const deleteButtonElement = document.createElement("button");
  deleteButtonElement.textContent = "削除";

  checkboxElement.addEventListener("change", () => {
    toggleTaskDone(index);
    saveTasks();
    renderTasks();
  });

  deleteButtonElement.addEventListener("click", () => {
    deleteTask(index);
    saveTasks();
    renderTasks();
  });

  taskItemElement.appendChild(checkboxElement);
  taskItemElement.appendChild(taskTextElement);
  taskItemElement.appendChild(deleteButtonElement);

  return taskItemElement;
}
JavaScript

そして renderTasks は、こうシンプルになります。

function renderTasks() {
  taskListElement.textContent = "";

  tasks.forEach((task, index) => {
    const taskItemElement = createTaskItemElement(task, index);
    taskListElement.appendChild(taskItemElement);
  });
}
JavaScript

こうすると、

renderTasks は「tasks をループして要素を追加するだけ」
createTaskItemElement は「タスク1件分の見た目とイベントを担当」

という役割分担がはっきりします。


関数の「大きさ」と「責任範囲」を見直す

大きすぎる関数を分割し、小さすぎる関数はまとめる

関数分割でよくある失敗は、
「とにかく細かく分ければいい」と思ってしまうことです。

例えば、こんな分け方はやりすぎです。

テキスト要素を作るだけの関数
チェックボックスを作るだけの関数
削除ボタンを作るだけの関数

こうなると、逆にコードを追うのが大変になります。

Day29 で目指したいのは、

タスク1件分の DOM を作る → 1つの関数
タスクの状態を変える → 1つの関数ごとに役割を分ける

という「ほどよい粒度」です。

createTaskItemElement は「タスク1件」という意味のある単位なので、
関数として独立させる価値があります。


命名と並び順で「読む順番」を整える

上から読んだときにストーリーになるように並べる

コードは「読むもの」です。
読むときの流れを意識して、関数の並び順を整えます。

例えば、次のような順番にすると、
上から読んだときに自然なストーリーになります。

データ定義(tasks など)
データ操作系(addTask / deleteTask / toggleTaskDone)
保存・復元系(saveTasks / loadTasks)
描画系(createTaskItemElement / renderTasks)
イベント登録(addButton の click など)
初期化処理(loadTasks / renderTasks の呼び出し)

この順番にしておくと、
「上から読めばアプリの全体像が分かる」状態になります。

命名も同じで、

addTask
deleteTask
toggleTaskDone

のように、「何をする関数か」が一目で分かる名前にしておくと、
後から読んだときのストレスが一気に減ります。


重複している処理を見つけてまとめる

save → render のセットを意識する

TODO アプリでは、

データを変える
→ saveTasks
→ renderTasks

という流れが何度も出てきます。

チェック変更時
削除ボタン押下時
追加ボタン押下時

この「お決まりの流れ」を、
一つの関数にまとめることもできます。

例えば、こういう関数を用意します。

function updateAndRender(updater) {
  updater();
  saveTasks();
  renderTasks();
}
JavaScript

そして、使う側はこう書けます。

checkboxElement.addEventListener("change", () => {
  updateAndRender(() => {
    toggleTaskDone(index);
  });
});

deleteButtonElement.addEventListener("click", () => {
  updateAndRender(() => {
    deleteTask(index);
  });
});

addButtonElement.addEventListener("click", () => {
  const text = taskInputElement.value.trim();
  if (text === "") {
    return;
  }

  updateAndRender(() => {
    addTask(text);
  });

  taskInputElement.value = "";
});
JavaScript

ここまでやるかどうかは好みですが、
「同じパターンが何度も出てきたら、名前をつけてあげる」
という発想は、関数分割の重要な感覚です。


例題:関数分割前と後を比べてみる

分割前の renderTasks を頭の中で再現する

関数分割前の renderTasks は、だいたいこんな感じでした。

タスクリストを空にする
forEach で tasks を回す
タスク1件分の div を作る
チェックボックスを作る
テキスト要素を作る
完了状態に応じてスタイルを変える
削除ボタンを作る
チェックボックスに change イベントを付ける
削除ボタンに click イベントを付ける
全部を親要素に append する
親要素を taskList に append する

これが 1 関数の中に全部入っていると、
読む側はかなり疲れます。

分割後の構造を確認する

関数分割後は、こうなります。

renderTasks
タスクリストを空にする
tasks を forEach で回し、createTaskItemElement を呼ぶ
返ってきた要素を append する

createTaskItemElement
タスク1件分の DOM を作る
完了状態に応じてスタイルを変える
イベントを登録する
親要素を返す

このように、「何をしているか」が
関数名とコードのまとまりで自然に伝わるようになります。


Day29 後半の完成イメージ

全体を通して読めるコードにする

ここまでの整理を反映した JavaScript の構造イメージは、こうなります。

const taskInputElement = document.getElementById("taskInput");
const addButtonElement = document.getElementById("addButton");
const taskListElement = document.getElementById("taskList");

let tasks = [];

function addTask(text) {
  const task = {
    title: text,
    isDone: false
  };
  tasks.push(task);
}

function deleteTask(index) {
  tasks.splice(index, 1);
}

function toggleTaskDone(index) {
  const task = tasks[index];
  task.isDone = !task.isDone;
}

function saveTasks() {
  const json = JSON.stringify(tasks);
  localStorage.setItem("tasks", json);
}

function loadTasks() {
  const json = localStorage.getItem("tasks");

  if (json === null) {
    tasks = [];
    return;
  }

  tasks = JSON.parse(json);
}

function createTaskItemElement(task, index) {
  const taskItemElement = document.createElement("div");

  const checkboxElement = document.createElement("input");
  checkboxElement.type = "checkbox";
  checkboxElement.checked = task.isDone;

  const taskTextElement = document.createElement("span");
  taskTextElement.textContent = task.title;

  if (task.isDone) {
    taskTextElement.style.textDecoration = "line-through";
    taskTextElement.style.color = "#888";
  } else {
    taskTextElement.style.textDecoration = "none";
    taskTextElement.style.color = "inherit";
  }

  const deleteButtonElement = document.createElement("button");
  deleteButtonElement.textContent = "削除";

  checkboxElement.addEventListener("change", () => {
    toggleTaskDone(index);
    saveTasks();
    renderTasks();
  });

  deleteButtonElement.addEventListener("click", () => {
    deleteTask(index);
    saveTasks();
    renderTasks();
  });

  taskItemElement.appendChild(checkboxElement);
  taskItemElement.appendChild(taskTextElement);
  taskItemElement.appendChild(deleteButtonElement);

  return taskItemElement;
}

function renderTasks() {
  taskListElement.textContent = "";

  tasks.forEach((task, index) => {
    const taskItemElement = createTaskItemElement(task, index);
    taskListElement.appendChild(taskItemElement);
  });
}

addButtonElement.addEventListener("click", () => {
  const text = taskInputElement.value.trim();

  if (text === "") {
    return;
  }

  addTask(text);
  saveTasks();
  renderTasks();
  taskInputElement.value = "";
});

loadTasks();
renderTasks();
JavaScript

上から読んでいくと、

データの定義
データ操作
保存・復元
描画の部品
描画本体
イベント登録
初期化

という流れが自然に追えるはずです。


Day29 後半のまとめ

Day29 後半で押さえてほしいポイントは、次の通りです。

タスク1件分の DOM を作る処理を createTaskItemElement に切り出した
renderTasks を「tasks を回して要素を追加するだけ」のシンプルな関数にした
関数の粒度を「意味のある単位」に揃えた
命名と並び順で「上から読めるストーリー」を作った
同じパターン(データ変更 → 保存 → 再描画)を意識して整理した

ここまで来ると、TODO アプリは
「動くコード」から「育てていけるコード」に変わっています。

この感覚は、どんなアプリを作るときにも必ず役に立ちます。
機能を足す前に「まず設計と整理をする」という癖を、ここで自分のものにしておきましょう。

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