ishikawa_pro's memorandum

若手webエンジニアの備忘録です.

Swift Node.js Docker AWS etc...色々やります。

社内で見かけるNode.jsデザインパターン

こんにちは。id:ishikawa_proです。
この記事は、CAM Advent Calendar 1日目の記事です。
会社でAdvent Calendar をやりたいと僕が言い出したので、1番手をいただきました!

僕は、今年CAMの新卒として入社し、サーバーサイドエンジニアとして、主にNode.jsを書いてきました。
今回は、業務でNode.jsのアプリケーションを書いていて、理解する上で重要だなと個人的に思ったデザインパターンを紹介したいと思います。

本題

僕は入社してから主に2つの既存サービスにジョインして開発などに携わってきました。
今回は弊社のNode.jsアプリケーションを読んでいて、理解するのに重要だなと思った

  • Singleton
  • DI

の2つのデザインパターンについて、Node.jsでの実装と弊社での使い方について紹介します。

Singleton

まずは、Singletonについてです。

singletonとは

Singletonとは、

  • そのクラスのインスタンスが1つしか生成されないことを保証する

ためのGoFデザインパターンの1つです。
ja.wikipedia.org

Node.jsでの実装

javascriptはmodule pattern自体がsingletonに則しているので、特別意識していなくともsingletonになっています。

// hoge.js
module.exports = {
  hoge: 'hoge'
};
// fuga.js
const hoge = require('./hoge');
console.log(hoge.hoge);
module.exports = hoge;
// main.js
const hoge = require('./hoge');

console.log(hoge.hoge);
hoge.hoge = 'fuga';
console.log(hoge.hoge);

const fuga = require('./fuga');
// ログ
hoge //main.jsの最初のconsole.log
fuga //main.jsの2番目のconsole.log
fuga //fuga.jsのconsole.log

main.js で hoge.jsのObjectのvalueを書き換えてから、fuga.jsをrequireするとfuga.js内でrequireしたhoge.jsの値がmain.jsで書き換えた値になっています。
1度 require されたモジュールはキャッシュされることを利用して、javascriptでは簡単にsingletonを実装することができます。

下記のように、classをインスタンス化したものをexportすることで、よりsingletonらしい(?)書き方もできます。

class Person {
  constructor() {
    this.name = 'ishikawa-pro';
  };

  greet() {
    console.log('My name is ishikawa-pro!!');
  }
}
module.exports = new Person();

社内で使っている所

弊社のNode.jsアプリケーションでは、ORMや別のアプリケーションとやりとりするためのclient ライブラリなどを管理するためにContextという名前のClassをよく用います。
このContextクラスでは色々なmoduleのインスタンス管理などをしており、各所でcontext classの新しいインスタンス生成をされては困るため、先ほどの例のようにContext classを宣言してインスタンスをexportしています。
そしてアプリケーション起動時にContextが初期化され、各controllerなどではcontext経由で色々なライブラリ呼び出して使っています。

// context.js
const hogeApiClient = require('./api_client');
const mysqlClient = require('./mysql_client');

class Context {
  constructor() {
    this.mysql = mysqlClient();
    this.hogeApiClient = hogeApiClient();
  }
}

module.exports = new Context();
// app.js
const express = require('express');
const context = require('./context');
const app = express();

app.get('/person', (req, res) => {
  const {Person} = context.mysql.instance.models;
  Person.findAll().then(res => {
    res.send(res);
  });
});

app.listen(3000);

上の例は結構いい加減ですが(ごめんなさい🙇‍♂️)、 だいたいこのようにcontext経由でORMなどを呼び出したりして使っています。

実際の業務では様々かつ大量のmoduleを使っており、それらのインスタンスなどのライフサイクルを一元管理するためにSingletonを使っています。

注意点

singletonは保守性やテスタビリティの観点から使い所が難しいと言われています。 なので業務で使う時は、グローバル変数としてメソッド間で状態を共有するような使い方などはせず、あくまで必要なライブラリを呼び出すためのToolBox的な役割が大きいです。

DI(Dependency Injection)

続いては、DIについてです。

DIとは

DI自体は定番のパターンなので詳しい説明は避けますが、

  • そのモジュールが依存するオブジェクト(設定ファイルなど)を外部から注入する

ことです。
依存するオブジェクトを差し替えられるにすることで、テスト時はmockに差し替えたり、環境ごとに異なった設定ファイルを読み込ませることが簡単にできるようになります。

ja.wikipedia.org

取り上げておいてなんですが、そもそもjavascriptは動的型付け言語なので型に縛られることもないし、sinonのstubなどを使ってしまえばDIを使わずにテスト時に依存するオブジェクトを差し替えることが可能なので、実際そんなに活躍することはないかもしれないです。
たまに型で縛ってDIとかでカチッと実装したい時もありますが、そんな時はいっそTypeScriptに移行しましょう。笑 (jsで使えるDIコンテナライブラリもありすが、、、)

Node.jsでの実装

それでもたまに使うことがあるので、Node.jsでの実装を書いてみます。
以下はconstructor injectionのパターンです。

// config_development.js
module.exports = {
  db: {
    mysql: {
      adress: 127.0.0.1,
      port: 3306
    }
  }
};
// mysql_client.js
const MySQLClient = require('MySQLClient');

module.exports = (config) => {
  return new MySQLClient(config);
};
// index.js
const config = require('./config_development');
const db = require('./mysql_client')(config.db);

Configのオブジェクトをexportしたmoduleと、configを引数で受けるようになっているmoduleがあり、index.jsでそれぞれをロードして組み立てています。
こうすることで、devとstg環境で異なった情報を渡して初期化したりなどが容易になります。

社内で使っている所

業務では、先ほどのsingletonで紹介したContextを、DIで注入して使っている場合があります。

// src/context.js

const services = require('./services');

class Context {
  constructor() {
    this.services = services(this);
  }
}
module.exports = new Context();
// src/services/index.js
modules.exports = context => {
  retrun {
    user: require('./user')(context),
  };
};
// src/services/user.js
module.exports = context => {
  class User {
    // 色々contextに依存したロジック
  }
  return User;
};
なぜここでDIが必要か

一見、別にDIで注入しなくてもContextはSingletonなんだから、

// src/services/user.js

const context = require('../context');

class User {
  // 色々ロジックを書く
}
return User;

のように、使うところで毎回requireすれば良さそうに見えます。
ですが、context moduleがservicesをrequireしており、services以下のmoduleで context をrequireしてしまうと循環参照がおきてしまいます。
循環参照がおきてしまうと、参照先のmoduleはまだ評価中なので空のオブジェクトが返ってきてしまい、思い通りの挙動にならいのです。

その他にも環境ごとにconfigを読み分けたりなどにもDIを使っていますが、今回はjavascript独特な問題を解決するための手段として使っている部分を取り上げみました。

まとめ

いかがだったでしょうか?
普段、既存サービスに機能を追加したりなどしかしていないと、下回りがどうなっているかを意識しなくなりがちだったので、いつも触っているアプリケーションでどんなことをしているかを理解するために必要そうなデザインパターン取り上げてみました。

まだまだNode.jsについて分からないことだらけなので、今後もどんどん深掘って勉強していきます!

明日は、 先輩フロントエンドエンジニアMuuKojima さんの
Web Componentsがフルに導入されたプロジェクトに入った結果。
です!
お楽しみに!!


参考文献