Shinosaka.rb Advent Calendar 2016 の 5日目の記事です。
はじめに
「外部のリソースサーバなどからデータを取得する」などのユースケースを考えたときに、本番環境であればS3から取得する、ステージング環境であれば特定のサーバからFTPする、開発・テスト環境であればローカルのデータやモックを使いたいなどデプロイ先の環境に応じて切替えたいということがあると思います。
実現方法はいくつかあると思いますが、 今年のshinosaka.rb の AdventCalendar でも同様の記事があるのでとても参考になると思います!!
よくある実装パターン
このような仕様を実装する場合のよくあるパターンとしては Rails.env.production?
の分岐により切替えることができます。例えば以下のような感じです。
class TodosController < ApplicationController
def show
If Rails.env.production?
# Procuction環境の処理
else
# Production以外の処理
end
end
# ...省略
end
このパターンでもアプリケーションが単純なうちはいいのですが、規模が大きくなりこのようなif分岐がいたるところで出てくると運用保守という点でとてもやりにくくなることが想像できると思います。(stagingという env がでてきたらどうするかなど)
そんなとき ダックタイピング と DI によって環境ごとに切替えられると便利そうだということで、 railsconfig/config を使って簡易的なDIコンテナとして実現してみたので紹介します。
用語について
DIとは
DIとDIコンテナについては以下の記事がとてもわかり易かったので、そちらを参照してみてください。
- DI・DIコンテナ、ちゃんと理解出来てる・・? - Qiita
- やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or something
railsconfig/config とは
使っている方も多いと思いますが、グローバルな定数を設定できるツールです。
アクセストークンなどを定義しておいてコード上からは Settings.access_token
などでどこからでも参照して使えるといったものです。
参考実装の構成
サンプルコードはここにおいています。
yusabana-sandbox/dirails-sample
APP_ROOT
├── app/
│ ├── clients/
│ │ └── ad/
│ │ ├── development_contents.rb <= DIする依存クラス(本番以外の環境用)
│ │ └── production_contents.rb <= DIする依存クラス(本番用)
│ ├── controllers/
│ │ ├── todos_controller.rb
│ │ └── ...
│ └── ...
│
├── config/
│ ├── settings.yml
│ ├── settings.local.yml
│ ├── settings/ <= Rails.envによって切替えたい内容を記述する
│ │ ├── development.yml
│ │ ├── production.yml
│ │ └── test.yml
│ ├── application.rb
│ └── ...
└── ...
DIで切り替えるクラスを定義
まずは例としてAd(広告)のコンテンツ取得クラスがあるとします。それを各環境向けのクラスとして定義しておきます。コンテンツの取得ロジックなどは環境ごとに違うという想定です。
class Ad::ProductionContents
def extract
# ここに本番環境独自のコンテンツ取得のコードを書く
'production contents'
end
end
class Ad::DevelopmentContents
def extract
# ここに本番環境以外のコンテンツ取得コードを書く。
# または以下のようにダミー文字列を返してスタブしてもいい
'development contents'
end
end
上記のように定義された各環境向けの コンテンツ取得クラス
を railsconfig/config で動的に差し替えられるようにします。
railsconfig/config で参照するClientsクラスを切替える
railsconfig の設定ファイルをyamlでかく環境ごとに定義
各環境ごとの設定を定義して、クラス名を書くことで依存クラスを指定します。
# このファイルは Rails.env が production のときにのみ読み込まれる
clients_class: 'Ad::ProductionContents'
# このファイルは Rails.env が development のときにのみ読み込まれる
clients_class: 'Ad::DevelopmentContents'
実際に利用するコード
例えばコントローラなどで、依存を記述した railsconfigの Settings
からクラスを取得して実装するコードを書きます。
以下のようにControllerのクラスでは特に Rails.env
の環境のことは意識しないで取得できます。
class TodosController < ApplicationController
def show
@ad_contents = Settings.clients_class.constantize.new.extract
end
# ...省略
end
-
Settings.clients_class.constantize.new
の部分-
production
のときは production向けのAd::ProductionContents
がインスタンス化され、それ以外のときはAd::DevelopmentContents
がインスタンス化されるようになります。
-
- 上記の通りインスタンス化して、共通のインターフェースの
extract
メソッドで各環境に応じたコンテンツが取得できます。
まとめ
Ad::ProductionContents
と Ad::DevelopmentContents
は共通インターフェースとなる extract
メソッドを持たせてダックタイピングをしつつ、 Settings(railsconfig) によってダックタイピングの依存オブジェクトを切替えて利用することが出来るようになります。
- メリット
- ダックタイピングにより新たな env を作ったとしても特に呼び出し側(ここでは
TodosController
)は変更せずとも、XXXContents
クラスを追加するだけで機能追加できます。 - コード内でのif分岐がなくなります。(
if Rails.env.prodution?
) など - 依存オブジェクトをSettings(簡易DIコンテナ)で管理しているので依存オブジェクトをきりかえるなどが柔軟にできます。
- ダックタイピングにより新たな env を作ったとしても特に呼び出し側(ここでは
- デメリット
- 各環境のクラスがふえてきて実装コードがファイルレベルで分散してまうことで複雑度は増します。
(が、、慣れてしまえばRailsの規約同様、チームで開発しても同じような実装になり運用することはやりやすくなると思います。)
- 各環境のクラスがふえてきて実装コードがファイルレベルで分散してまうことで複雑度は増します。
最後に、、サンプル実装この内容に関しては以下のコミットで確認ができます。
Shinosaka.rb Advent Calendar 2016 の 5日目の記事でした。明日は、、、「空き」です。(誰か...)
以上です。