2016年3月4日
依存関係をなくそう : Rubyアプリ・Gemの開発者への提言
(2016-02-09)by Mike Perham
本記事は、原著者の許諾のもとに翻訳・掲載しております。
本記事はRubyについて書かれたものではありますが、Python、JavaScript、Javaなど、全ての言語コミュニティに当てはまる事実を述べたものです。依存関係が引き起こす負の連鎖は誰のためにもなりません。
上の図は、私がこれまでに使用した全てのRailsアプリの依存関係を可視化したものです。以下の例はいずれも、どこかで聞いたことのあるものではないでしょうか。
- 何百ものエントリを含むGemfile
- 本番環境で読み込まれるテスト用Gem
- 数百メガバイトもRAMを食うRailsのプロセス
Rubygemsシステムは、それを再利用する誰もが容易にRubyのパッケージを作ることができるという点で、賞賛に値するものです。しかし、その便利さが意味するところは、そうしたGemと他のGemを非常に安易に結び付け、さらにそれが、「インターネットでダウンロード」され、数百もの依存関係を持つRailsアプリにつながるということです。
Rubygemを公開するときには、あなたのGemを使用している全てのアプリとの依存関係がどんどん雪だるま式に増えていくということになります。これにより、そうしたGemに含まれるバグが及ぼす影響は倍数的に増えることになるのです。
mime-typesの興味深いケース
mime-types
というGemは最近、 メモリ使用量を最適化 し、数メガバイトもRAMを軽くすることができました。この最適化による恩恵はまさに、既存のRailsアプリ全てにあると言えます。なぜなら、結果的にRailsはmime-typesのGemに依存しているからです。 rails -> actionmailer -> mail -> mime-types
つまり、このGemはアプリによって使われていたわけではなかったのです。RailsもActionMailerもこのGemを直接は使用していませんでした。このGemは、ActionMailerの実装の奥深くで使われていて、 あまりにも多くのメモリを使用していました 。存在するあらゆるのRailsアプリが、この問題により10MBものメモリを使用していたのです。
アプリの開発者たち、よく聞いて!
依存関係のせいで、あなたのアプリは膨れ上がり、不安定になり、モンキーパッチやバグの多いネイティブコードがおかしな振る舞いを引き起こす可能性があります。Railsアプリに依存関係を追加する場合、まずはさっとサニティチェックをするといいでしょう。望ましい順番に以下にリストします。
- これって本当に要るの? 切りましょう。
- 自分で最低限必要な機能を実装できる? やってみよう。
Gemが必要な場合は次のようになります。
1.そのGemはネイティブ拡張を含んでいる? Pure Rubyの代替案を考えよう。
2.そのGemはいもづる式に他の多数のGemを引き込む? もっとシンプルな代替案を探そう。
ネイティブ拡張を備えたGemはシステムを不安定にする可能性があり、原因不明のバグやクラッシュの原因になりかねません。必要な価値以上の依存関係を引き込むGemは避けましょう。悪いGemの例に fog
があります。このGemは39個ものGemを引き込みます。これはRails自体よりも多い依存関係であり、そのうちの多くは不必要です。
最後になりますが、本当に必要なGemだけを読み込むようにしましょう。Bundlerのグループサポートを使って、テストしていない時にはテスト用のGemをオフにしておきましょう。
group :test do
gem 'rspec'
gem 'timecop'
# etc
end
Gemの開発者たち、よく聞いて!
ライブラリの作者として、あなたはユーザとアプリケーションに敬意を持って接するべきです。そもそもの依存関係を最低限にする努力をしてください。そうすれば彼らのアプリケーションに対し余分なコードを読み込ませたり、問題を起こしたりしなくても済むでしょう。自身のコードはコントロールできても、依存関係はコントロールできません。ライブラリの依存関係にバグがあれば、ユーザや彼らのアプリに問題を引き起こすバグになるかもしれません。
あなたはGemの開発者として、各Gemの依存関係について以下を把握していますか?
- どれだけのメモリを必要とするか。
- requireにどのくらい時間がかかるか。
- モジュールの中や外でモンキーパッチを行うかどうか。
Sidekiqはあれだけの機能を備えながら、たった3つのランタイムの依存関係しかありません。 concurrent-ruby
、 connection_pool
と redis
です。
Die json, die(ドイツ語で「The json, the」)
(訳注:英語では”jsonくたばれ”です)
余りにも多くのGemがJSONやOJ、multi_json、yajl-rubyなどへの依存関係を宣言しています。JSONの周囲には骨化した粗雑な作りのレイヤがたくさんあります。これに対して為すべきことはただ1つ、全てなくしてしてしまいましょう。JSONは1.9の頃からstdlibの中にずっと存在していました。依存関係を宣言する必要は全くありません。ただ require 'json'
として、あとはRubyに処理させましょう。
Railsでもできたんですから あなたにもできますよ。
すでに使えるものがあるのに、なぜHTTPクライアントを選択するのでしょう?
Railsアプリは全て、faraday、rest-client、httparty、excon、typhoeus、curbなど6種類ものHTTPクライアントに依存しています。その理由は、様々なGemが内部的にこうしたクライアントを利用しているからです。 RubyGemが内部的に使ってもいいのはNet::HTTPだけです! Net::HTTP APIを学んで、依存関係を削除して、HTTPクライアント関連のGemについて、ユーザに余計な負担を強いるのもやめましょう。
curbを使って最適化したバージョンを提供したい? いいでしょう。でもオプションとして提供してください。つまり、アプリケーションの開発者がcurbを選択できるようにするのはいいのですが、Net::HTTPを常にデフォルトとするべきです。
Rails 5.0の最適化
ここ数週間、私はRails 5.0でGemへの依存関係を最低限に抑えるための作業に取り組んでいます(他の開発者、@_matthewdや@applerebelと協力しながら進めています。)。Rails 4.2.5は34個のGemをrequireしています。Rails 5.0b1は55個、Rails 5.0b2は39個のGemをrequireします。私としては、Rails 5.0に使うGemを37個以下にできると考えています。これまでのところ私たちは、Celluloid、EventMachine、thread_safe、jsonを削除しました。
残念ながら、容易に削除できるものはもう残っていません。Nokogiriはぜひ削除したいです。大量のネイティブな拡張コンポーネントの中に、大きな依存関係が含まれているからです。しかしその 依存関係の中に、重要なもの が幾つか含まれています。 Oga は使いやすく、より簡潔な選択肢です。Nokogiriに依存するGemを公開する場合、それはオプションとして、デフォルトはREXML(読者の皆さんの言いたいことは分かります、でも少なくともこれはstdlibに含まれるものです)またはOgaとすることを検討してください。
ソリューションの一部となる
Rails 5.0を改良することは私にもできますが、Gem全ての問題を解決するのは無理です。あなたがGemの開発者であれば、そのGemの依存関係を確認して、可能な限り削除してください。あなたがアプリの開発者であれば、Gemfileを調査して、1つか2つは削除できないものかどうか検討してください。とにかく必要なのは単純化です。
例えば Gemの1つである「Stripe」 で、2つ含まれているランタイムの依存関係は両方とも削除できると、私は思います。
覚えておくべきルール
ソフトウェア設計時のル―ルを幾つか挙げておきます。
- 動作を速くしたいなら、コードをなくすに越したことはない。
- バグを減らしたいなら、コードをなくすに越したことはない。
- 必要なメモリ容量を減らしたいなら、コードをなくすに越したことはない。
- コードを分かりやすくしたいなら、コードをなくすに越したことはない。
依存関係はなくしましょう。そうすれば、Gemもアプリも改善されます。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa