node.jsにおけるcircular dependency問題に対処するための3つの方法

node.js でコードを書いている時にハマった circular dependency という問題とその対処法について紹介します。

circular dependency

この問題は cyclic dependency とも呼ばれ、日本語だと 循環参照 とか 循環依存関係と呼びます。これはどういう問題かというと、あるファイルを require したときにその結果が空のオブジェクトとして返される問題 です。 この問題に直面した時はなんでこうなるのか全く理解できなかったのですが、よくよくコードを見てみると、あるファイルとあるファイルがお互いにrequireし合っていることに気づき、色々調べてみるとこういう問題があることがわかりました。

以下にこの問題を再現させるサンプルコードを示します。

これらのコードを見ると、main.jsがa.jsを、a.jsがb.jsを、b.jsがa.jsをそれぞれrequireしていますね。 main.js を実行してみると、以下のように実行エラーになります。cyclic dependency のため A というオブジェクトが空オブジェクトになっています。空なので、getId というキーでアクセスしても undefined が返っています。

この問題は、モジュールAが読み込まれる前にそのモジュール自身が読み込まれた時に発生します。

上のサンプルコードの require の流れに着目してみましょう。

main.js(2行目)var A = require(‘./a’);
→→a.js(3行目)var B = require(‘./b’);
→→→→b.js(3行目)var A = require(‘./a’);
→→→→b.js 読み込み完了
→→a.js 読み込み完了

流れを見てわかるように、a.js の読み込みが完了する前に b.js の中で a.js が読み込まれています。a.js の読み込みは完了していないので、b.js の3行目では空のオブジェクトが返っています。

ではどうすべきか

この問題はかなりデバッグしづらいです。できれば事前にみつけたいですね。この問題を事前に見つけるためには、CIで自動テストを継続的に実行させることが重要ですね。ソースコードを眺めていてもこの問題を見つけるのが難しいためです。

こういう問題が起こること自体、ある意味モジュール化の仕方に問題があるとも言えるのですが、がっつり設計し直すことも難しい状況もあると思うので、 大きく構造を変えずにこの問題を解決させる方法について紹介してみます。方法の名前は勝手に命名していますw

方法1:module.exports first

require(‘./b’) する前に module.exports を書く方法です。

オリジナルのa.jsと上のコードを比べてみると、真っ先にmodule.exportsにオブジェクトを代入しています。requireよりも先にモジュールAをexportすることで、circluar dependencyを回避しています。

この方法でコードを書き換える場合、コーディングスタイルによっては上の例のように大幅なファイル変更が必要になる場合があります。ちなみに、普段からmodule.exports firstでコードを書く癖をつけておくと、circluar dependencyの問題にハマる可能性もグッと下がるのでオススメです。

方法2:lazy require

利用したいモジュールを先頭で require するのではなく、必要なときに初めてrequireする方法です。

この方法は最も既存のコードへの影響が少ない方法です。 このファイルがrequireされるときに、init関数自体は実行されないので、cyclic dependency は回避出来ています。init関数が呼ばれて初めて、b.jsが読み込まれます。

関数呼び出しのたびに、requireされるのでオーバーヘッドが若干気になりますが、 requireされた結果はキャッシュされるので大きなパフォーマンスの低下はないはずです。

実際のソースコード

方法3:dependency injection

dependency injection(DI; 依存性の注入)は、モジュール間の結合度を下げるためのデザインパターンの一種です。

「モジュールBがモジュールAを利用する」という依存関係をソースコードにハードコードするのではなく、外部からモジュールBの引数に注入しています。こうすることで、b.jsはa.jsをrequireする必要がなくなるということです。

上の例だとモジュールのI/Fに若干の変更は入りますが、DIのテクニックを活用することで、モジュール間の結合度が下がり、モジュールのテストが書きやすくなるので、こちらもいい方法なのかなと思います。

まとめ

node.js における circular dependency という問題とその対処法について紹介しました。一度この問題にハマって解決方法を知るとそれ以降は用心深くなれるのですが、知らないとかなり苦労します。まだこの問題にハマっていない方も是非知っておいて損はないと思います!

Reference

東京Node学園祭2014 #nodefest

東京Node学園祭2014に参加してきました。

感想ですが、Node学園祭には初参加でしたが、色んな発表を見れたので勉強になりました。海外スピーカーの発表を中心に聞いていました。そこまで聞き取れたわけではないので、リスニングの勉強の必要性を痛感しました。。 次は何らかの形で発表できたらなと思います。

Node.jsコミッターを多く抱えているStrongloop CEOのIssac Roth(@ijroth)さんに英語で話して、グッズをもらえたのはいい思い出です。

以下は各発表の感想と資料をリンクしています。

Continue reading

passport-yj 1.0.5 released

Yahoo!JAPANのOAuth認証(YConnect)を簡単に行うためのnpm packageである passport-yj に pull request が来ていた。 依存モジュールである oauth2 のソースコードの中身が変わっていたのか、install後に実行されるパッチが期待通りに適用されていなかった。 動作確認も問題なかったのでマージ。

あと動作確認したときに、同梱しているサンプルで正常にユーザ情報を表示できていなかったので、それも合わせて修正。 どうやらYConnect の userinfo API のレスポンス仕様が変わっていることに気づいた。

作ってから1年も経つと、ライブラリのバージョンが上がったりして思わぬところで問題が起こるのだな。

passport-yj 1.0.5

Open Hack Day でnode.jsのPassportを使ってYahoo!JAPANと楽天のOAuth認証モジュールを作った

概要

今週の土日に開催された Open Hack Day Japan という Hackathon イベントに参加して来ました。主催は Yahoo! JAPAN です。会社の同期と二人で参加しました。

紹介(Passport)

私たちがつくったものは node.js で OAuth 認証を超簡単に実装できるモジュールです。このモジュールは Passport という OAuth 認証モジュールのラッパーのような形で提供しています。

現在、Facebook, Twitter 等の数多くのウェブサービスのラッパーが提供されており、今回新たに Yahoo!JAPAN と楽天のラッパーを作成したという位置付けです。

成果物

使い方

yconnect を例に使い方を説明したいと思います。 リポジトリの examples/login/app.js で簡単に試せます。

1.アプリケーション登録

Yahoo!デベロッパーネットワーク からアプリケーション登録します。

  • アプリケーションID
  • シークレット

を取得します。ちゃんとコールバックURIも登録します。

2. インストール

リポジトリを git clone して、config.js というファイルを編集します。 先ほど取得したアプリケーションID、シークレット、コールバックURIを書いてください。 そして、npm install で依存モジュールをインストールします。

3. 起動

あとは express を起動するだけです。

これで http://localhost:3000 にアクセスすれば試すことができます。

今後

ただひとつ問題があります。yconnect でアクセストークンを取得するときに、client ID と client secret を Authorization ヘッダに付与して Basic 認証を行なうという仕様になっています。しかしながら、この認証方法は他の OAuth2.0 の実装と比較して特殊で、node の oauth モジュールがこの認証方式に対応していません。

そのため、passport-yj では npm の postinstall の時に oauth モジュールの oauth2.js にパッチを当てるという処置を暫定的に行なっています。

もちろん、このような処置はあまりイケていないので、oauth モジュールの作者に Pull Request を送って、エレガントにこの問題を解決したいと思います(続く…かも)。

2013/02/21 1:17
Pull Request 送りました!
Client Authentication via Basic Authorization, not body by gologo13 · Pull Request #126 · ciaranj/node-oauth


まとめ

  • Yahoo!JAPANと楽天向けの Passport を使った OAuth 認証モジュールの開発
  • GitHub で OSS として公開!
  • npm モジュールとして公開!