JavaScript | ES6+ 文法:モジュール – モジュール分割設計

JavaScript JavaScript
スポンサーリンク

モジュール分割設計とは何か(まずイメージから)

モジュール分割設計は、
「アプリ全体を、意味のある小さな“部品(ファイル)”に分けて、その関係をきれいに整理すること」 です。

1ファイル = 1モジュール、として
「このファイルは何を担当するのか?」
「どのファイルがどのファイルに依存してよいのか?」
を決めていく作業そのものが「モジュール設計」です。

ここが重要です。
最初から「正解の分割」を知っている人はいません。
大事なのは、
「1ファイルに全部書く」のをやめて、
「役割ごとに分けてみて、足りないところは後から調整する」
という感覚を持つことです。

まず決めるべきは「何ごとに 1 モジュールか」

機能ごとに分ける(feature 単位)

一番イメージしやすいのは「機能ごと」です。

例えば、簡単な ToDo アプリを作るなら:

タスクに関する処理
ユーザーに関する処理
API 通信
画面の初期化(エントリーポイント)

など、機能ごとに分けるイメージです。

todo/
  todoModel.js      ← タスクデータのクラスや操作
  todoView.js       ← 画面にタスクを表示する処理
  todoController.js ← イベント処理(クリックされたらどうするか)
  api.js            ← サーバーとの通信
  main.js           ← アプリのエントリーポイント

「todoModel.js はタスクデータのことだけ考える」
「todoView.js は見た目のことだけ考える」
というように、1ファイルに「考えること」を絞るイメージです。

概念ごとに分ける(ドメイン単位)

少しだけ進んだ考え方として、「アプリの中の“名詞”」ごとに分ける方法もあります。

ユーザー(User)
商品(Product)
注文(Order)

といった「実世界の概念」がはっきりしているなら、
それぞれに1モジュールを割り当てるのも自然です。

models/
  User.js
  Product.js
  Order.js

このとき、「User.js はユーザーという概念のことだけ考える」
「Order.js は注文という概念のことだけ考える」
というふうに、1モジュール=1概念にします。

ここが重要です。
「何ごとに 1 モジュールにするか」を一言で説明できると、分割の軸がブレにくくなります。
機能単位でも概念単位でも構いませんが、「自分のプロジェクトにとってしっくりくる粒度」を探していきます。

依存の向きをどう設計するか(上流と下流のイメージ)

「下にあるほど基礎」「上にあるほどアプリ寄り」

モジュール同士の関係は、矢印でイメージすると分かりやすいです。

例えば:

util(小さな共通関数)

models(データ・ルール)

services(アプリのビジネスロジック)

ui / main(画面・エントリーポイント)

という「層」を作るイメージです。

画面は services に依存してよい、
services は models に依存してよい、
models は util に依存してよい。

でも、逆方向の依存はできるだけ避けるようにします。
(例えば、util が UI に依存する、など)

具体的な構成例を見てみます。

src/
  util/
    date.js
  models/
    Todo.js
  services/
    todoService.js
  ui/
    todoView.js
  main.js

main.js はアプリ全体の入口として、他を組み合わせる役です。

循環しないように「片方向」に揃える

理想は、モジュール同士の矢印が「片方向だけ」に揃っている状態です。

例えば:

util/date.jsmodels/Todo.jsservices/todoService.jsui/todoView.jsmain.js

このように、一方向に流れていれば循環参照も起きにくく、
「どっちからどっちを使ってよいか」が分かりやすくなります。

ここが重要です。
「どの層がどの層に依存してよいか」を自分なりに一度決めると、
後からモジュールを追加するときに迷いが減り、循環参照の事故も起こりにくくなります。

1 モジュールは「何を export して、何を隠すか」

モジュールの「外向きの顔」を決める

各モジュールには、「外に見せる API」と「中で完結させるロジック」があります。

例えば、タスクモデルのモジュールなら:

// todoModel.js
let nextId = 1;                     // モジュールスコープ内の状態(外から隠す)

export class Todo {
  constructor(title) {
    this.id = nextId++;
    this.title = title;
    this.done = false;
  }

  toggle() {
    this.done = !this.done;
  }
}

export function createTodo(title) { // 外に見せたい窓口
  return new Todo(title);
}
JavaScript

nextId のような内部だけで使う状態は export せず、
Todo クラスや createTodo のように「使ってもらいたいもの」だけを export します。

他のモジュールは、TodocreateTodo を知っていれば十分で、
nextId の存在を気にしなくて済みます。

ここが重要です。
モジュール分割は、「ファイルを分けること」以上に
「外から見せたいインターフェース(export)と、中の実装(非 export)を分けること」 が本質です。

index.js を使って「窓口」を作る

関連するモジュールが増えてきたら、
index.js で再エクスポートして入口を整理する方法もあります。

models/
  Todo.js
  User.js
  index.js
// models/Todo.js
export class Todo { /* ... */ }
JavaScript
// models/User.js
export class User { /* ... */ }
JavaScript
// models/index.js
export { Todo } from "./Todo.js";
export { User } from "./User.js";
JavaScript

これで、他のファイルからは

import { Todo, User } from "./models/index.js";
JavaScript

のように使えます。

「models の中身がどう分割されているか」は外から意識しなくてよくなり、
フォルダの中を整理し直しても、index.js さえ合わせて直せば外の import はそのままにできます。

小さな例で「モジュール設計の流れ」を通してみる

例:シンプルな ToDo アプリを段階的に分割する

最初にありがちなのは、全部 main.js に書くパターンです。

// main.js(何も分割していない状態)
const todos = [];

function addTodo(title) { /* 追加 */ }
function toggleTodo(id) { /* 完了切り替え */ }
function render() { /* DOM 更新 */ }

document.getElementById("addBtn").addEventListener("click", () => {
  const title = document.getElementById("titleInput").value;
  addTodo(title);
  render();
});

render();
JavaScript

ここから少しずつ分割していきます。

段階1:データ操作を別モジュールへ

// todoStore.js
const todos = [];

export function addTodo(title) {
  todos.push({ id: Date.now(), title, done: false });
}

export function toggleTodo(id) {
  const todo = todos.find((t) => t.id === id);
  if (todo) todo.done = !todo.done;
}

export function getTodos() {
  return todos;
}
JavaScript
// main.js
import { addTodo, toggleTodo, getTodos } from "./todoStore.js";

function render() {
  const list = document.getElementById("list");
  list.innerHTML = "";
  for (const todo of getTodos()) {
    const li = document.createElement("li");
    li.textContent = todo.title + (todo.done ? "(完了)" : "");
    li.addEventListener("click", () => {
      toggleTodo(todo.id);
      render();
    });
    list.appendChild(li);
  }
}

// イベント設定や画面のことだけ考える
document.getElementById("addBtn").addEventListener("click", () => {
  const title = document.getElementById("titleInput").value;
  addTodo(title);
  render();
});

render();
JavaScript

この段階で、

todoStore.js は「タスクの配列を管理する役」
main.js は「DOM やイベントを扱う役」

と役割が分かれました。

段階2:さらに view の責任を分離する

// todoView.js
export function renderTodos(todos, { onToggle }) {
  const list = document.getElementById("list");
  list.innerHTML = "";
  for (const todo of todos) {
    const li = document.createElement("li");
    li.textContent = todo.title + (todo.done ? "(完了)" : "");
    li.addEventListener("click", () => onToggle(todo.id));
    list.appendChild(li);
  }
}
JavaScript
// main.js
import { addTodo, toggleTodo, getTodos } from "./todoStore.js";
import { renderTodos } from "./todoView.js";

function render() {
  renderTodos(getTodos(), {
    onToggle(id) {
      toggleTodo(id);
      render();
    },
  });
}

document.getElementById("addBtn").addEventListener("click", () => {
  const title = document.getElementById("titleInput").value;
  addTodo(title);
  render();
});

render();
JavaScript

ここまで来ると:

todoStore.js は「データ」
todoView.js は「表示」
main.js は「全体の流れ(イベントと render の呼び出し)」

という風に、それぞれのモジュールの「仕事」がかなりハッキリしてきます。

ここが重要です。
最初から完璧に分割しようとしなくていいので、
「全部1ファイル」→「データと画面を分ける」→「画面の描画をさらに切り出す」
のように、少しずつ段階を踏んでモジュールを育てていくのが現実的です。

モジュール設計で意識しておきたいチェックポイント

「このファイルは一言で何を担当しているか?」と言えるか

モジュール分割がうまくいっているかどうかは、
各ファイルについて「このファイルは一言で言うと何をしている?」と自問してみると分かります。

「ユーザーデータの定義と、その操作をまとめたモジュール」
「API との通信だけを担当するモジュール」
「画面のレンダリングだけを担当するモジュール」

一言でスッと言えるなら、そのモジュールはだいたい良い形になっています。
逆に、「あれもこれもやってて説明しにくい…」と感じるなら、
中身を2つ以上のモジュールに分けるサインです。

import の方向がごちゃごちゃしていないか

モジュール同士の import が、

上から下へ ばかりなのか
あっちこっちから、あっちこっちへ 行っていないか

を軽く意識してみてください。

同じフォルダ内で互いを import しあっているファイルが多いときは、
「共通の土台モジュールを作る」「さらに上の階層に組み合わせ役を作る」チャンスです。

名前付き export と default export の使い分け

モジュールの「主役」がはっきりしているなら default export、
複数の機能をまとめて提供したいなら名前付き export、
という風に使い分けると、モジュールの意図が読みやすくなります。

例えば:

Todo.js なら export default class Todo
todoStore.js なら export function addTodo / export function getTodos のように名前付き

といった感じです。

まとめ

ES6 モジュールの「モジュール分割設計」の核心は、
「ファイルを意味のある単位で分け、依存の向きと export を通して“誰が何を知っていてよいか”をデザインすること」 です。

ポイントを整理すると:

何ごとに 1 モジュールか(機能単位・概念単位など)をまず決める
モジュール同士の依存が一方向に流れるように「層」を意識する
1 モジュールの中で「外に見せる export」と「中だけで使う実装」を分ける
関連モジュールが増えたら index.js で再エクスポートして窓口をまとめる
最初から完璧は目指さず、「1ファイル → ざっくり分割 → さらに整理」と段階的に進める

練習としては、今 1 ファイルにまとまっているコードを
「データ」「画面」「通信」の 3 つぐらいに分けてみることから始めるとちょうど良いです。

そのとき、「このファイルは一言でいうと何?」を必ず自分に問いかけてみてください。
それを繰り返していくと、モジュールの“形”を見る目がどんどん育っていきます。

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