最近Node.jsを学び始めたのですが、最初に学んだ概念のひとつがモジュールでした。私は数年間フロントエンド開発者として働いてきましたが、モジュールが表面的にはどのようなものか知ってはいましたが、深く理解することはありませんでした。今まで。

モジュールは本質的にコードのファイルにすぎません。それが最もシンプルな表現です。しかし、モジュールが実際に何であり、なぜ存在し、どのように使うのかを深く掘り下げていくと、解き明かすべきことがたくさんあります。

Node.jsのモジュールは、あらゆるアプリケーションの構成要素です。なぜなら、次のことを実現するからです:

  1. コードを管理しやすいファイルに整理する
  2. カプセル化を提供する
  3. グローバル名前空間の汚染を防ぐ
  4. コードの保守性と再利用性を向上させる

これらについて詳しく見ていきましょう。

1. コードを管理しやすいファイルに整理する

その核心は関心の分離であり、各コードが一つのことだけを担当すべきだという考え方です。

簡単な例を見てみましょう。基本的な数学演算がすべて単一のファイルapp.jsに存在する場合:

function add(x, y) {
  return x + y;
}

function subtract(x, y) {
  return x - y;
}

function multiply(x, y) {
  return x * y;
}

console.log(`Sum is: ${add(5, 3)}`); // Output: 8
console.log(`Subtraction is: ${subtract(5, 3)}`); // Output: 2
console.log(`Multiplication is: ${multiply(5, 3)}`); // Output: 15

Enter fullscreen mode Exit fullscreen mode

これは小さくてシンプルな例で、それ自体では特に乱雑に見えません。しかし、モジュールを使うことで、よりきれいに整理し、成長しても保守しやすくすることができます。

そのために、数学演算のみを扱う新しいファイルmath.jsを作成します:

function add(x, y) {
  return x + y;
}

function subtract(x, y) {
  return x - y;
}

function multiply(x, y) {
  return x * y;
}

module.exports = { add, subtract, multiply };

Enter fullscreen mode Exit fullscreen mode

数学演算をこの新しいファイルに移動しただけです。これらをアプリケーションの他の場所で使用するには、それらをエクスポートする必要があります。Node.jsではこれを行う方法がいくつかあります。ここでは、Node.jsが最初からデフォルトで使用しているモジュールシステムであるCommonJSを使用しています(新しい標準であるES Modulesとは異なり、この記事では取り上げません)。

これらの関数をapp.jsで使用するには、Nodeの組み込みrequire()関数を使用してインポートします:

const { add, subtract, multiply } = require('./math');

console.log(`Sum is: ${add(5, 3)}`); // 8
console.log(`Subtraction is: ${subtract(5, 3)}`); // 2
console.log(`Multiplication is: ${multiply(5, 3)}`); // 15

Enter fullscreen mode Exit fullscreen mode

これで、app.jsは数学がどのように行われるかを知る必要がなくなり、それらの関数をrequireして使用できることだけを知っていればよくなりました。数学ロジックは一か所にあり、明確な責任を持っています。

2. カプセル化

カプセル化とは、何かがどのように動作するかの内部の詳細を隠し、コードの他の部分が実際に必要とするものだけを公開することを意味します。モジュールを使用する人は、その内部変数やヘルパーロジックを知ったり触れたりする必要はなく、パブリックインターフェイスのみを知っていればよいのです。

簡単なケースを見てみましょう:内部状態を直接公開するカウンターモジュールで、インポートするものは適切な関数を通さずにその状態を直接変更できます。

// counter.js
let count = 0;

module.exports = { count };

Enter fullscreen mode Exit fullscreen mode

// app.js
const counter = require('./counter');

counter.count = 100; // Oops. Nothing stopped this.
console.log(counter.count); // 100

Enter fullscreen mode Exit fullscreen mode

外部から直接不正にcountが変更されるのを防ぐものは何もありません。

カプセル化されたバージョン

// counter.js
let count = 0;

function increment() {
  count++;
  return count;
}

function getCount() {
  return count;
}

module.exports = { increment, getCount };

Enter fullscreen mode Exit fullscreen mode

// app.js
const counter = require('./counter');

counter.increment();
counter.increment();
console.log(counter.getCount()); // 2

Enter fullscreen mode Exit fullscreen mode

これでcountは直接アクセスできなくなりました。それに触れることができるのは、モジュールが明示的に公開することを選択した関数であるincrement()getCount()を通じてのみです。

これにより、内部状態が予期しないまたは無効な方法で変更されるのを防ぎ、increment()getCount()が同じように動作する限り、内部実装(countの保存方法や更新方法)が後で変更されても、モジュールに依存するものを壊すことはありません。

3. グローバル名前空間の汚染を防ぐ

モジュールがない場合、スクリプトで宣言するすべての変数と関数が同じグローバルスコープを共有する可能性があります。2つのファイル(または2つのライブラリ)が偶然同じ変数名を使用した場合、一方が他方を静かに上書きし、追跡が難しいバグを引き起こす可能性があります。

たとえば、モジュールシステムがない状態で、両方ともdataという変数を使用する2つの別々のファイルを想像してみてください:

// file1.js
var data = 'user info';

Enter fullscreen mode Exit fullscreen mode

// file2.js
var data = 'product info';

Enter fullscreen mode Exit fullscreen mode

これらのファイルの両方が同じグローバルスコープで実行される場合(同じページ上の2つの<script>タグ、または古いスタイルのNodeセットアップで一緒に読み込まれる2つのファイルなど)、2番目のdataが最初のものを上書きします。最後に実行されるファイルが「勝ち」、何かが間違っていたという警告はありません。

Node.jsモジュールは、各ファイルに独自のスコープを与えることでこの問題を解決します。モジュール内で宣言されたものは、明示的にエクスポートされない限り、そのファイルにプライベートに保たれます:

// file1.js
const data = 'user info';
module.exports = { data };

Enter fullscreen mode Exit fullscreen mode

// file2.js
const data = 'product info';
module.exports = { data };

Enter fullscreen mode Exit fullscreen mode

// app.js
const file1 = require('./file1');
const file2 = require('./file2');

console.log(file1.data); // user info
console.log(file2.data); // product info

Enter fullscreen mode Exit fullscreen mode

各モジュールが独自の独立したスコープを持っているため、両方のdata変数は安全に共存できます。明示的にエクスポートしない限り、何もグローバルスコープに漏れません。

4. 保守性と再利用性の向上

コードが責任ごとにモジュールに分割されると、2つのことが自然に簡単になります:保守再利用です。

保守性は、各モジュールが明確で単一の目的を持っているために向上します。価格の計算方法にバグが見つかった場合、アプリケーション全体をスキャンするのではなく、価格設定モジュールという正確な場所を探せばよいのです。

再利用性は、よく書かれたモジュールが誰が使用するかを気にしないために向上します。先ほどのmath.jsの例に戻ると:そのファイルは完全に異なるプロジェクトにインポートでき、元のアプリに固有のものへの依存関係がないため、まったく同じように動作します。

// Could be reused in any project, unchanged
const { add, subtract, multiply } = require('./math');

Enter fullscreen mode Exit fullscreen mode

これがこれまでカバーしてきたすべてのことの成果です:コードの整理、内部詳細のカプセル化、グローバルスコープの競合の回避はすべて、個別に保守しやすく、プロジェクト間で再利用しやすいモジュールにつながります。

結論

フロントエンドのバックグラウンドから来て、私は常にモジュールを十分に理解していると思っていました。それらはただのファイルですよね?しかし、なぜそれらが存在するのかを深く掘り下げたことで、コードの構造化についての考え方が変わりました。モジュールは単に整理のためにファイルを分割するだけではありません。関心の分離、内部状態の保護、命名の競合の回避、そして時間の経過とともに保守および再利用しやすいコードの構築に関するものです。

これは表面をなぞっただけです。CommonJSとES Modulesの違い、Nodeでのモジュールキャッシュの仕組み(モジュールはrequireする回数に関係なく、一度だけ読み込まれ実行される)、またはNodeがモジュールパスを内部でどのように解決するかなど、探求すべきことがまだたくさんあります。しかし、どのようにに飛び込む前に、なぜモジュールが重要なのかを理解したことで、残りの学習がはるかに速く理解できるようになりました。

Node.jsを学んでいる方も、モジュールについてどのように考えているか、何が(何が)理解できたかを聞かせていただけると嬉しいです。